孟苏慧

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);
......
......@@ -3,10 +3,12 @@ package com.infoloop.tianting.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.infoloop.tianting.SingleCOperatorRpcResponse;
import com.infoloop.tianting.SingleDishSkuByIdsRpcResponse;
import com.infoloop.tianting.clientcustomerorderservice.OnlinePayEnum;
import com.infoloop.tianting.clientcustomerorderservice.OrderOperationRecord;
import com.infoloop.tianting.clientcustomerorderservice.OrderOperationType;
import com.infoloop.tianting.clientcustomerorderservice.OrderSourceEnum;
import com.infoloop.tianting.clientcustomerorderservice.OrderStatusEnum;
import com.infoloop.tianting.clientcustomerorderservice.PayStatusEnum;
import com.infoloop.tianting.clientcustomerorderservice.ServeStatusEnum;
......@@ -39,6 +41,7 @@ import com.infoloop.tianting.service.client.ClientInventoryServiceRpcClient;
import com.infoloop.tianting.service.client.ClientResourceTagServiceRpcClient;
import com.infoloop.tianting.service.client.MealServiceRpcClient;
import com.infoloop.tianting.service.client.MenuServiceRpcClient;
import com.infoloop.tianting.service.client.OperatorServiceRpcClient;
import com.infoloop.tianting.service.client.OrderServiceRpcClient;
import com.infoloop.tianting.service.client.SkuServiceRpcClient;
import com.infoloop.tianting.setmealscheduleservice.SingleMealRpcResponse;
......@@ -92,6 +95,7 @@ public class SettlementServiceImpl implements SettlementService {
private final MealServiceRpcClient mealServiceRpcClient;
private final ClientCustomerServiceRpcClient clientCustomerServiceRpcClient;
private final ClientResourceTagServiceRpcClient clientResourceTagServiceRpcClient;
private final OperatorServiceRpcClient operatorServiceRpcClient;
public static ByteArrayOutputStream writeToExcelBySku(SettlementDataDto settlementDataDto)
throws IOException {
......@@ -347,6 +351,14 @@ public class SettlementServiceImpl implements SettlementService {
cell1.setCellValue("订单号");
cell1.setCellStyle(headerStyle);
Cell cellPayTime = titleRow.createCell(columnNum++);
cellPayTime.setCellValue("支付时间");
cellPayTime.setCellStyle(headerStyle);
Cell cellOrderer = titleRow.createCell(columnNum++);
cellOrderer.setCellValue("下单人");
cellOrderer.setCellStyle(headerStyle);
Cell cell2 = titleRow.createCell(columnNum++);
cell2.setCellValue("餐别");
cell2.setCellStyle(headerStyle);
......@@ -422,6 +434,14 @@ public class SettlementServiceImpl implements SettlementService {
dataCell1.setCellValue(orderDetail.getCode() != null ? orderDetail.getCode() : "");
dataCell1.setCellStyle(dataStyle);
Cell dataCellPayTime = dataRow.createCell(columnNum++);
dataCellPayTime.setCellValue(orderDetail.getPayTime() != null ? orderDetail.getPayTime() : "");
dataCellPayTime.setCellStyle(dataStyle);
Cell dataCellOrderer = dataRow.createCell(columnNum++);
dataCellOrderer.setCellValue(orderDetail.getOrderer() != null ? orderDetail.getOrderer() : "");
dataCellOrderer.setCellStyle(dataStyle);
Cell dataCell2 = dataRow.createCell(columnNum++);
dataCell2.setCellValue(orderDetail.getMealName() != null ? orderDetail.getMealName() : "");
dataCell2.setCellStyle(dataStyle);
......@@ -511,7 +531,7 @@ public class SettlementServiceImpl implements SettlementService {
}
// 自动调整列宽
for (int i = 0; i < 17; i++) {
for (int i = 0; i < 19; i++) {
sheet.autoSizeColumn(i);
}
......@@ -838,6 +858,16 @@ public class SettlementServiceImpl implements SettlementService {
final List<SingleClientCustomerRpcResponse> customers = customerResponse.getResponsesList();
final var customerMap = customers.stream().collect(Collectors.toMap(SingleClientCustomerRpcResponse::getId, c -> c, (c1, c2) -> c1));
final List<Integer> stallOperatorIds = orderIds.stream()
.map(orderMap::get)
.filter(Objects::nonNull)
.filter(o -> o.getCreationSource() == OrderSourceEnum.STALL)
.map(SingleClientCustomerOrderRpcResponse::getCreatedBy)
.filter(id -> id > 0)
.distinct()
.collect(Collectors.toList());
final Map<Integer, String> operatorNameById = buildStallOperatorDisplayNameMap(stallOperatorIds);
// 构建结算订单明细
final var classifyOrders = new ArrayList<SettlementDTO.SettlementOrderDetail>();
for (Integer orderId : orderIds) {
......@@ -854,6 +884,8 @@ public class SettlementServiceImpl implements SettlementService {
final var customer = customerMap.get(order.getCustomerId());
final String customerName = customer != null ? customer.getName() : "";
final String hisCustomerNo = customer != null ? customer.getHISCustomerNo() : "";
final String payTimeStr = formatSettlementPayTime(order.getPayTime());
final String orderer = resolveSettlementOrderer(order, customer, operatorNameById);
final var orderOperationRecords = orderOperationRecordByOrderId.getOrDefault(orderId, Collections.emptyList());
......@@ -881,10 +913,10 @@ public class SettlementServiceImpl implements SettlementService {
&& addOriginalDetailMap.get(orderDetail.getId()).getCount() != orderDetail.getCount()) {
final var orderDetailBO = addOriginalDetailMap.get(orderDetail.getId());
SettlementDTO.SettlementOrderDetail beforeChange = buildSettlementOrderDetail(
order, orderDetail.getMenuDetailId(), orderDetailBO.getSkuId(), orderDetailBO.getCount(), orderDetail.getPrice(), "订餐", menuName, customerName, hisCustomerNo, menuDetailMap, mealMap, skuMap);
order, orderDetail.getMenuDetailId(), orderDetailBO.getSkuId(), orderDetailBO.getCount(), orderDetail.getPrice(), "订餐", menuName, customerName, hisCustomerNo, payTimeStr, orderer, menuDetailMap, mealMap, skuMap);
classifyOrders.add(beforeChange);
SettlementDTO.SettlementOrderDetail afterChange = buildSettlementOrderDetail(
order, orderDetail.getMenuDetailId(), orderDetailBO.getSkuId(), orderDetail.getCount() - orderDetailBO.getCount(), orderDetail.getPrice(), "加餐", menuName, customerName, hisCustomerNo, menuDetailMap, mealMap, skuMap);
order, orderDetail.getMenuDetailId(), orderDetailBO.getSkuId(), orderDetail.getCount() - orderDetailBO.getCount(), orderDetail.getPrice(), "加餐", menuName, customerName, hisCustomerNo, payTimeStr, orderer, menuDetailMap, mealMap, skuMap);
classifyOrders.add(afterChange);
}
......@@ -894,20 +926,20 @@ public class SettlementServiceImpl implements SettlementService {
final var orderDetailBO = replaceOriginalDetailMap.get(orderDetail.getId());
final var orderDetailAdjustBO = replaceAdjustDetailMap.get(orderDetail.getId());
SettlementDTO.SettlementOrderDetail beforeChange = buildSettlementOrderDetail(
order, orderDetail.getMenuDetailId(), orderDetailBO.getSkuId(), orderDetailBO.getCount(), orderDetailBO.getPrice(), "换菜前", menuName, customerName, hisCustomerNo, menuDetailMap, mealMap, skuMap);
order, orderDetail.getMenuDetailId(), orderDetailBO.getSkuId(), orderDetailBO.getCount(), orderDetailBO.getPrice(), "换菜前", menuName, customerName, hisCustomerNo, payTimeStr, orderer, menuDetailMap, mealMap, skuMap);
classifyOrders.add(beforeChange);
SettlementDTO.SettlementOrderDetail afterChange = buildSettlementOrderDetail(
order, orderDetail.getMenuDetailId(), orderDetail.getSkuId(), orderDetail.getCount(), orderDetailAdjustBO.getPrice(), "换菜后", menuName, customerName, hisCustomerNo, menuDetailMap, mealMap, skuMap);
order, orderDetail.getMenuDetailId(), orderDetail.getSkuId(), orderDetail.getCount(), orderDetailAdjustBO.getPrice(), "换菜后", menuName, customerName, hisCustomerNo, payTimeStr, orderer, menuDetailMap, mealMap, skuMap);
classifyOrders.add(afterChange);
}
// 3. 判断已退款
if (orderDetail.getServeStatus() == ServeStatusEnum.SERVE_REJECT) {
SettlementDTO.SettlementOrderDetail refundRecord1 = buildSettlementOrderDetail(
order, orderDetail.getMenuDetailId(), orderDetail.getSkuId(), orderDetail.getCount(), orderDetail.getPrice(), "订餐", menuName, customerName, hisCustomerNo, menuDetailMap, mealMap, skuMap);
order, orderDetail.getMenuDetailId(), orderDetail.getSkuId(), orderDetail.getCount(), orderDetail.getPrice(), "订餐", menuName, customerName, hisCustomerNo, payTimeStr, orderer, menuDetailMap, mealMap, skuMap);
classifyOrders.add(refundRecord1);
SettlementDTO.SettlementOrderDetail refundRecord2 = buildSettlementOrderDetail(
order, orderDetail.getMenuDetailId(), orderDetail.getSkuId(), -orderDetail.getCount(), orderDetail.getPrice(), "已退款", menuName, customerName, hisCustomerNo, menuDetailMap, mealMap, skuMap);
order, orderDetail.getMenuDetailId(), orderDetail.getSkuId(), -orderDetail.getCount(), orderDetail.getPrice(), "已退款", menuName, customerName, hisCustomerNo, payTimeStr, orderer, menuDetailMap, mealMap, skuMap);
classifyOrders.add(refundRecord2);
}
......@@ -920,7 +952,7 @@ public class SettlementServiceImpl implements SettlementService {
if (isNormal) {
SettlementDTO.SettlementOrderDetail normalRecord = buildSettlementOrderDetail(
order, orderDetail.getMenuDetailId(), orderDetail.getSkuId(), orderDetail.getCount(), orderDetail.getPrice(), "订餐", menuName, customerName, hisCustomerNo, menuDetailMap, mealMap, skuMap);
order, orderDetail.getMenuDetailId(), orderDetail.getSkuId(), orderDetail.getCount(), orderDetail.getPrice(), "订餐", menuName, customerName, hisCustomerNo, payTimeStr, orderer, menuDetailMap, mealMap, skuMap);
classifyOrders.add(normalRecord);
}
}
......@@ -950,6 +982,8 @@ public class SettlementServiceImpl implements SettlementService {
String menuName,
String customerName,
String hisCustomerNo,
String payTimeStr,
String orderer,
Map<Integer, SingleMenuDetailRpcResponse> menuDetailMap,
Map<Integer, SingleMealRpcResponse> mealMap,
Map<Integer, SingleDishSkuByIdsRpcResponse> skuMap) {
......@@ -962,6 +996,8 @@ public class SettlementServiceImpl implements SettlementService {
return SettlementDTO.SettlementOrderDetail.builder()
.mealTime(order.getMealTime() != 0L ? LocalDateTimeUtil.of(order.getMealTime()).format(DateUtil.YYYY_MM_DD_LEFT_SYMBOL) : "")
.code(order.getOrderCode())
.payTime(payTimeStr != null ? payTimeStr : "")
.orderer(orderer != null ? orderer : "")
.mealName(mealName)
.roomNo(roomNo)
.customerName(customerName)
......@@ -1067,6 +1103,69 @@ public class SettlementServiceImpl implements SettlementService {
}
}
private Map<Integer, String> buildStallOperatorDisplayNameMap(List<Integer> operatorIds) {
if (operatorIds == null || operatorIds.isEmpty()) {
return Collections.emptyMap();
}
final List<SingleCOperatorRpcResponse> operators = operatorServiceRpcClient.getCOperatorsByIds(operatorIds);
final Map<Integer, String> map = new HashMap<>();
for (SingleCOperatorRpcResponse op : operators) {
if (op != null && op.getId() != 0) {
map.put(op.getId(), stallOperatorDisplayName(op));
}
}
return map;
}
private static String stallOperatorDisplayName(SingleCOperatorRpcResponse op) {
if (op == null) {
return "";
}
if (StringUtils.isNotBlank(op.getAlias())) {
return op.getAlias();
}
if (StringUtils.isNotBlank(op.getName())) {
return op.getName();
}
return "";
}
private static String formatSettlementPayTime(long payTimeMillis) {
if (payTimeMillis == 0L) {
return "";
}
final String formatted = DateUtil.formatDate(payTimeMillis, DateUtil.Y_M_D_H_M_S);
return formatted != null ? formatted : "";
}
/**
* 院区端(STALL):操作员名称;住院部小程序(MINI_PROGRAM):客户姓名;阳光厅(MIDDLE_PLATFORM):客户手机号。
*/
private static String resolveSettlementOrderer(SingleClientCustomerOrderRpcResponse order,
SingleClientCustomerRpcResponse customer,
Map<Integer, String> stallOperatorNameById) {
if (order == null) {
return "";
}
return switch (order.getCreationSource()) {
case STALL -> {
int createdBy = order.getCreatedBy();
if (createdBy <= 0 || stallOperatorNameById == null) {
yield "";
}
String name = stallOperatorNameById.get(createdBy);
yield name != null ? name : "";
}
case MINI_PROGRAM -> customer != null && StringUtils.isNotBlank(customer.getName())
? customer.getName()
: "";
case MIDDLE_PLATFORM -> customer != null && StringUtils.isNotBlank(customer.getMobile())
? customer.getMobile()
: order.getOpenId();
default -> "";
};
}
/**
* 计算单价
*/
......