feat: 芯烨云打印机接入
This commit is contained in:
1 parent
9681a18bd1
commit
1cc030d0db
19 files changed
+465
-17
No files matched your search
+5
@@ -41,6 +41,11 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode WECHAT_SHIPPING_DELIVERY_NO_REQUIRED = new ErrorCode(1008007026, "请填写同城配送单号");
|
ErrorCode WECHAT_SHIPPING_DELIVERY_NO_REQUIRED = new ErrorCode(1008007026, "请填写同城配送单号");
|
||||||
ErrorCode WECHAT_SHIPPING_UPLOAD_FAILED = new ErrorCode(1008007027, "微信发货信息上报失败");
|
ErrorCode WECHAT_SHIPPING_UPLOAD_FAILED = new ErrorCode(1008007027, "微信发货信息上报失败");
|
||||||
|
|
||||||
|
ErrorCode STORE_ORDER_PRINT_DISABLED = new ErrorCode(1008007028, "芯烨云打印未启用或未配置开发者凭证");
|
||||||
|
ErrorCode STORE_ORDER_PRINT_NOT_PAID = new ErrorCode(1008007029, "订单未支付,无法打印");
|
||||||
|
ErrorCode STORE_ORDER_PRINT_TYPE_NOT_SUPPORT = new ErrorCode(1008007030, "仅支持自取/外卖订单打印");
|
||||||
|
ErrorCode STORE_ORDER_PRINT_SN_EMPTY = new ErrorCode(1008007031, "门店未配置打印机SN");
|
||||||
|
ErrorCode STORE_ORDER_PRINT_FAILED = new ErrorCode(1008007032, "打印失败:{}");
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
+8
-1
@@ -157,6 +157,13 @@ public class StoreOrderController {
|
|||||||
return success(storeOrderService.orderNotice());
|
return success(storeOrderService.orderNotice());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/printOrder")
|
||||||
|
@Operation(summary = "补打小票")
|
||||||
|
@Parameter(name = "id", description = "订单编号", required = true)
|
||||||
|
@PreAuthorize("@ss.hasPermission('order:store-order:update')")
|
||||||
|
public CommonResult<Boolean> printOrder(@RequestParam("id") Long id) {
|
||||||
|
storeOrderService.printStoreOrder(id);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+3
@@ -33,5 +33,8 @@ public interface RedisKeyConstants {
|
|||||||
"yshop_web_print_machine_cache:%s", // 参数为访问shopid
|
"yshop_web_print_machine_cache:%s", // 参数为访问shopid
|
||||||
STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
|
STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
|
||||||
|
|
||||||
|
RedisKeyDefine YSHOP_ORDER_PRINT_KEY = new RedisKeyDefine("订单打印幂等",
|
||||||
|
"yshop_order_print:%s", // 参数为 orderId
|
||||||
|
STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
|
||||||
|
|
||||||
}
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package co.yixiang.yshop.module.order.dal.redis.order;
|
||||||
|
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static co.yixiang.yshop.module.order.dal.redis.RedisKeyConstants.YSHOP_ORDER_PRINT_KEY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单自动打印幂等(防止支付回调重复出票)
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public class PrintOrderRedisDAO {
|
||||||
|
|
||||||
|
private static final long TTL_HOURS = 24;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true 表示首次标记成功,可以打印;false 表示已打印过
|
||||||
|
*/
|
||||||
|
public boolean tryMarkPrinted(String orderId) {
|
||||||
|
String redisKey = formatKey(orderId);
|
||||||
|
Boolean ok = stringRedisTemplate.opsForValue()
|
||||||
|
.setIfAbsent(redisKey, "1", TTL_HOURS, TimeUnit.HOURS);
|
||||||
|
return Boolean.TRUE.equals(ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatKey(String orderId) {
|
||||||
|
return String.format(YSHOP_ORDER_PRINT_KEY.getKeyTemplate(), orderId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
package co.yixiang.yshop.module.order.print.xpyun;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO;
|
||||||
|
import co.yixiang.yshop.module.order.dal.dataobject.storeordercartinfo.StoreOrderCartInfoDO;
|
||||||
|
import co.yixiang.yshop.module.order.enums.OrderLogEnum;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自取 / 外卖小票内容组装(58mm)
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class OrderReceiptBuilder {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter PAY_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
private static final String LINE = "--------------------------------<BR>";
|
||||||
|
|
||||||
|
public String build(StoreOrderDO order, List<StoreOrderCartInfoDO> items) {
|
||||||
|
boolean takeout = OrderLogEnum.ORDER_TAKE_OUT.getValue().equals(order.getOrderType());
|
||||||
|
String typeLabel = takeout ? "外卖" : "自取";
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("<C><B>").append(nullToEmpty(order.getShopName())).append("</B></C><BR>");
|
||||||
|
sb.append("<C>").append(typeLabel).append("</C><BR>");
|
||||||
|
if (order.getNumberId() != null) {
|
||||||
|
sb.append("<CB>#").append(order.getNumberId()).append("</CB><BR>");
|
||||||
|
}
|
||||||
|
sb.append(LINE);
|
||||||
|
|
||||||
|
sb.append("商品").append(pad("数量", 6)).append(pad("单价", 6)).append("<BR>");
|
||||||
|
sb.append(LINE);
|
||||||
|
if (items != null) {
|
||||||
|
for (StoreOrderCartInfoDO item : items) {
|
||||||
|
String name = nullToEmpty(item.getTitle());
|
||||||
|
if (StrUtil.isNotBlank(item.getSpec())) {
|
||||||
|
name = name + "(" + item.getSpec() + ")";
|
||||||
|
}
|
||||||
|
sb.append(name).append("<BR>");
|
||||||
|
Integer number = item.getNumber() == null ? 0 : item.getNumber();
|
||||||
|
BigDecimal price = item.getPrice() == null ? BigDecimal.ZERO : item.getPrice();
|
||||||
|
sb.append(" x").append(number)
|
||||||
|
.append(" ").append(price.toPlainString())
|
||||||
|
.append("<BR>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append(LINE);
|
||||||
|
|
||||||
|
if (order.getCouponPrice() != null && order.getCouponPrice().compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
sb.append("优惠:-").append(order.getCouponPrice().toPlainString()).append("<BR>");
|
||||||
|
}
|
||||||
|
if (takeout) {
|
||||||
|
BigDecimal freight = order.getPayPostage() != null ? order.getPayPostage()
|
||||||
|
: (order.getFreightPrice() != null ? order.getFreightPrice() : BigDecimal.ZERO);
|
||||||
|
sb.append("配送费:").append(freight.toPlainString()).append("<BR>");
|
||||||
|
}
|
||||||
|
BigDecimal payPrice = order.getPayPrice() == null ? BigDecimal.ZERO : order.getPayPrice();
|
||||||
|
sb.append("<R>实付:").append(payPrice.toPlainString()).append("元</R><BR>");
|
||||||
|
sb.append(LINE);
|
||||||
|
|
||||||
|
if (StrUtil.isNotBlank(order.getRealName())) {
|
||||||
|
sb.append("姓名:").append(order.getRealName()).append("<BR>");
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(order.getUserPhone())) {
|
||||||
|
sb.append("电话:").append(order.getUserPhone()).append("<BR>");
|
||||||
|
}
|
||||||
|
if (takeout && StrUtil.isNotBlank(order.getUserAddress())) {
|
||||||
|
sb.append("地址:").append(order.getUserAddress()).append("<BR>");
|
||||||
|
}
|
||||||
|
if (order.getPayTime() != null) {
|
||||||
|
sb.append("支付时间:").append(order.getPayTime().format(PAY_TIME_FMT)).append("<BR>");
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(order.getMark())) {
|
||||||
|
sb.append("备注:").append(order.getMark()).append("<BR>");
|
||||||
|
}
|
||||||
|
sb.append("订单号:").append(nullToEmpty(order.getOrderId())).append("<BR>");
|
||||||
|
sb.append("<BR><BR>");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String nullToEmpty(String s) {
|
||||||
|
return s == null ? "" : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String pad(String text, int width) {
|
||||||
|
if (text == null) {
|
||||||
|
text = "";
|
||||||
|
}
|
||||||
|
if (text.length() >= width) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return " ".repeat(width - text.length()) + text;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
package co.yixiang.yshop.module.order.print.xpyun;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@EnableConfigurationProperties(XpYunProperties.class)
|
||||||
|
public class XpYunConfiguration {
|
||||||
|
}
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
package co.yixiang.yshop.module.order.print.xpyun;
|
||||||
|
|
||||||
|
import cn.hutool.crypto.digest.DigestUtil;
|
||||||
|
import cn.hutool.http.HttpRequest;
|
||||||
|
import co.yixiang.yshop.framework.common.util.json.JsonUtils;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 芯烨云小票打印客户端
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class XpYunPrintClient {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private XpYunProperties properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印小票
|
||||||
|
*
|
||||||
|
* @param sn 打印机编号
|
||||||
|
* @param content 打印内容(标签语法)
|
||||||
|
* @param copies 份数
|
||||||
|
* @param voice 声音模式:0 取消 / 1 静音 / 2 来单
|
||||||
|
* @param mode 0 检查在线;1 离线也入队
|
||||||
|
* @return 云端响应
|
||||||
|
*/
|
||||||
|
public XpYunPrintResponse print(String sn, String content, int copies, int voice, int mode) {
|
||||||
|
Map<String, Object> body = buildCommonParams();
|
||||||
|
body.put("sn", sn);
|
||||||
|
body.put("content", content);
|
||||||
|
body.put("copies", copies);
|
||||||
|
body.put("voice", voice);
|
||||||
|
body.put("mode", mode);
|
||||||
|
|
||||||
|
String url = properties.getBaseUrl() + "/xprinter/print";
|
||||||
|
String json = JsonUtils.toJsonString(body);
|
||||||
|
log.info("[XpYun][print][sn({})] request={}", sn, json);
|
||||||
|
String respText = HttpRequest.post(url)
|
||||||
|
.header("Content-Type", "application/json;charset=UTF-8")
|
||||||
|
.body(json)
|
||||||
|
.timeout(15000)
|
||||||
|
.execute()
|
||||||
|
.body();
|
||||||
|
log.info("[XpYun][print][sn({})] response={}", sn, respText);
|
||||||
|
return JsonUtils.parseObject(respText, XpYunPrintResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReady() {
|
||||||
|
return Boolean.TRUE.equals(properties.getEnabled())
|
||||||
|
&& properties.getUser() != null && !properties.getUser().isBlank()
|
||||||
|
&& properties.getUserKey() != null && !properties.getUserKey().isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> buildCommonParams() {
|
||||||
|
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||||
|
String sign = DigestUtil.sha1Hex(properties.getUser() + properties.getUserKey() + timestamp);
|
||||||
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
params.put("user", properties.getUser());
|
||||||
|
params.put("timestamp", timestamp);
|
||||||
|
params.put("sign", sign);
|
||||||
|
params.put("debug", "0");
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package co.yixiang.yshop.module.order.print.xpyun;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 芯烨云通用响应
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class XpYunPrintResponse {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0 表示成功
|
||||||
|
*/
|
||||||
|
private Integer code;
|
||||||
|
|
||||||
|
private String msg;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功时一般为打印订单号
|
||||||
|
*/
|
||||||
|
private String data;
|
||||||
|
|
||||||
|
private Long serverExecutedTime;
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return code != null && code == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package co.yixiang.yshop.module.order.print.xpyun;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 芯烨云打印配置
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties(prefix = "yshop.xpyun")
|
||||||
|
@Data
|
||||||
|
public class XpYunProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用芯烨云打印
|
||||||
|
*/
|
||||||
|
private Boolean enabled = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发者 ID(邮箱或开发者 ID)
|
||||||
|
*/
|
||||||
|
private String user = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开发者密钥
|
||||||
|
*/
|
||||||
|
private String userKey = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAPI 根地址
|
||||||
|
*/
|
||||||
|
private String baseUrl = "https://open.xpyun.net/api/openapi";
|
||||||
|
|
||||||
|
}
|
||||||
+13
@@ -67,6 +67,8 @@ import org.redisson.client.codec.StringCodec;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
@@ -568,6 +570,17 @@ public class AppStoreOrderServiceImpl extends ServiceImpl<StoreOrderMapper,Store
|
|||||||
productName,orderInfo.getShopName());
|
productName,orderInfo.getShopName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 支付成功后异步打印小票(事务提交后再触发,避免读到未提交状态)
|
||||||
|
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||||
|
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||||
|
@Override
|
||||||
|
public void afterCommit() {
|
||||||
|
asyncStoreOrderService.printOrderAsync(orderId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
asyncStoreOrderService.printOrderAsync(orderId);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+100
-6
@@ -1,16 +1,12 @@
|
|||||||
package co.yixiang.yshop.module.order.service.storeorder;
|
package co.yixiang.yshop.module.order.service.storeorder;
|
||||||
|
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
import cn.hutool.core.date.DatePattern;
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
|
||||||
import cn.hutool.core.util.NumberUtil;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import co.yixiang.yshop.framework.common.enums.OrderInfoEnum;
|
import co.yixiang.yshop.framework.common.enums.OrderInfoEnum;
|
||||||
import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserOrderCountVo;
|
import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserOrderCountVo;
|
||||||
import co.yixiang.yshop.module.member.service.user.MemberUserService;
|
import co.yixiang.yshop.module.member.service.user.MemberUserService;
|
||||||
import co.yixiang.yshop.module.order.controller.admin.storeorder.vo.ShoperOrderTimeDataVo;
|
import co.yixiang.yshop.module.order.controller.admin.storeorder.vo.ShoperOrderTimeDataVo;
|
||||||
import co.yixiang.yshop.module.order.controller.app.order.vo.AppStoreOrderQueryVo;
|
|
||||||
import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO;
|
import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO;
|
||||||
import co.yixiang.yshop.module.order.dal.dataobject.storeordercartinfo.StoreOrderCartInfoDO;
|
import co.yixiang.yshop.module.order.dal.dataobject.storeordercartinfo.StoreOrderCartInfoDO;
|
||||||
import co.yixiang.yshop.module.order.dal.mysql.storeorder.StoreOrderMapper;
|
import co.yixiang.yshop.module.order.dal.mysql.storeorder.StoreOrderMapper;
|
||||||
@@ -18,22 +14,28 @@ import co.yixiang.yshop.module.order.dal.mysql.storeordercartinfo.StoreOrderCart
|
|||||||
import co.yixiang.yshop.module.order.dal.redis.order.AsyncCountRedisDAO;
|
import co.yixiang.yshop.module.order.dal.redis.order.AsyncCountRedisDAO;
|
||||||
import co.yixiang.yshop.module.order.dal.redis.order.AsyncOrderRedisDAO;
|
import co.yixiang.yshop.module.order.dal.redis.order.AsyncOrderRedisDAO;
|
||||||
import co.yixiang.yshop.module.order.dal.redis.order.PrintMechinRedisDAO;
|
import co.yixiang.yshop.module.order.dal.redis.order.PrintMechinRedisDAO;
|
||||||
|
import co.yixiang.yshop.module.order.dal.redis.order.PrintOrderRedisDAO;
|
||||||
import co.yixiang.yshop.module.order.enums.OrderLogEnum;
|
import co.yixiang.yshop.module.order.enums.OrderLogEnum;
|
||||||
|
import co.yixiang.yshop.module.order.print.xpyun.OrderReceiptBuilder;
|
||||||
|
import co.yixiang.yshop.module.order.print.xpyun.XpYunPrintClient;
|
||||||
|
import co.yixiang.yshop.module.order.print.xpyun.XpYunPrintResponse;
|
||||||
import co.yixiang.yshop.module.order.service.storeorder.dto.OrderTimeDataDto;
|
import co.yixiang.yshop.module.order.service.storeorder.dto.OrderTimeDataDto;
|
||||||
import co.yixiang.yshop.module.product.service.storeproduct.StoreProductService;
|
import co.yixiang.yshop.module.product.service.storeproduct.StoreProductService;
|
||||||
import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO;
|
import co.yixiang.yshop.module.store.dal.dataobject.storeshop.StoreShopDO;
|
||||||
import co.yixiang.yshop.module.store.dal.mysql.storeshop.StoreShopMapper;
|
import co.yixiang.yshop.module.store.dal.mysql.storeshop.StoreShopMapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static co.yixiang.yshop.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||||
|
import static co.yixiang.yshop.module.order.enums.ErrorCodeConstants.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步订单 Service 实现类
|
* 异步订单 Service 实现类
|
||||||
*
|
*
|
||||||
@@ -60,6 +62,12 @@ public class AsynStoreOrderServiceImpl implements AsyncStoreOrderService {
|
|||||||
private StoreShopMapper storeShopMapper;
|
private StoreShopMapper storeShopMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private StoreOrderCartInfoMapper storeOrderCartInfoMapper;
|
private StoreOrderCartInfoMapper storeOrderCartInfoMapper;
|
||||||
|
@Resource
|
||||||
|
private PrintOrderRedisDAO printOrderRedisDAO;
|
||||||
|
@Resource
|
||||||
|
private XpYunPrintClient xpYunPrintClient;
|
||||||
|
@Resource
|
||||||
|
private OrderReceiptBuilder orderReceiptBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取某个用户的订单统计数据
|
* 获取某个用户的订单统计数据
|
||||||
@@ -236,9 +244,95 @@ public class AsynStoreOrderServiceImpl implements AsyncStoreOrderService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Async
|
||||||
|
public void printOrderAsync(String orderId) {
|
||||||
|
try {
|
||||||
|
printOrder(orderId, false);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[printOrderAsync][orderId({}) 自动打印失败]", orderId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void printOrder(String orderId, boolean force) {
|
||||||
|
if (StrUtil.isBlank(orderId)) {
|
||||||
|
if (force) {
|
||||||
|
throw exception(PARAM_ERROR);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!xpYunPrintClient.isReady()) {
|
||||||
|
failOrSkip(force, STORE_ORDER_PRINT_DISABLED, "芯烨云未启用或凭证未配置");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StoreOrderDO order = storeOrderMapper.selectOne(new LambdaQueryWrapper<StoreOrderDO>()
|
||||||
|
.eq(StoreOrderDO::getOrderId, orderId));
|
||||||
|
if (order == null) {
|
||||||
|
failOrSkip(force, STORE_ORDER_NOT_EXISTS, "订单不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!OrderInfoEnum.PAY_STATUS_1.getValue().equals(order.getPaid())) {
|
||||||
|
failOrSkip(force, STORE_ORDER_PRINT_NOT_PAID, "订单未支付");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String orderType = order.getOrderType();
|
||||||
|
if (!OrderLogEnum.ORDER_TAKE_IN.getValue().equals(orderType)
|
||||||
|
&& !OrderLogEnum.ORDER_TAKE_OUT.getValue().equals(orderType)) {
|
||||||
|
failOrSkip(force, STORE_ORDER_PRINT_TYPE_NOT_SUPPORT, "订单类型不支持打印:" + orderType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (order.getShopId() == null) {
|
||||||
|
failOrSkip(force, STORE_ORDER_PRINT_SN_EMPTY, "订单无门店");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StoreShopDO shop = storeShopMapper.selectById(order.getShopId());
|
||||||
|
if (shop == null || StrUtil.isBlank(shop.getUniprintId())) {
|
||||||
|
failOrSkip(force, STORE_ORDER_PRINT_SN_EMPTY, "门店未配置打印机SN");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验通过后再做幂等,避免空 SN / 未启用误占 key
|
||||||
|
if (!force && !printOrderRedisDAO.tryMarkPrinted(orderId)) {
|
||||||
|
log.info("[printOrder][orderId({}) 已打印过,跳过]", orderId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<StoreOrderCartInfoDO> items = storeOrderCartInfoMapper.selectList(
|
||||||
|
new LambdaQueryWrapper<StoreOrderCartInfoDO>()
|
||||||
|
.eq(StoreOrderCartInfoDO::getOid, order.getId()));
|
||||||
|
String content = orderReceiptBuilder.build(order, items);
|
||||||
|
try {
|
||||||
|
// mode=1 离线入队;voice=2 来单播报
|
||||||
|
XpYunPrintResponse resp = xpYunPrintClient.print(shop.getUniprintId(), content, 1, 2, 1);
|
||||||
|
if (resp == null || !resp.isSuccess()) {
|
||||||
|
String msg = resp == null ? "无响应" : StrUtil.blankToDefault(resp.getMsg(), "未知错误");
|
||||||
|
if (force) {
|
||||||
|
throw exception(STORE_ORDER_PRINT_FAILED, msg);
|
||||||
|
}
|
||||||
|
log.error("[printOrder][orderId({}) 打印失败] {}", orderId, msg);
|
||||||
|
} else {
|
||||||
|
log.info("[printOrder][orderId({}) 打印成功] data={}", orderId, resp.getData());
|
||||||
|
}
|
||||||
|
} catch (co.yixiang.yshop.framework.common.exception.ServiceException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (force) {
|
||||||
|
throw exception(STORE_ORDER_PRINT_FAILED, e.getMessage());
|
||||||
|
}
|
||||||
|
log.error("[printOrder][orderId({}) 打印异常]", orderId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void failOrSkip(boolean force,
|
||||||
|
co.yixiang.yshop.framework.common.exception.ErrorCode errorCode,
|
||||||
|
String logMsg) {
|
||||||
|
if (force) {
|
||||||
|
throw exception(errorCode);
|
||||||
|
}
|
||||||
|
log.warn("[printOrder][skip] {}", logMsg);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
+13
-9
@@ -1,11 +1,5 @@
|
|||||||
package co.yixiang.yshop.module.order.service.storeorder;
|
package co.yixiang.yshop.module.order.service.storeorder;
|
||||||
|
|
||||||
import co.yixiang.yshop.module.member.controller.app.user.vo.AppUserOrderCountVo;
|
|
||||||
import co.yixiang.yshop.module.order.controller.admin.storeorder.vo.ShoperOrderTimeDataVo;
|
|
||||||
import co.yixiang.yshop.module.order.controller.app.order.vo.AppStoreOrderQueryVo;
|
|
||||||
import co.yixiang.yshop.module.order.dal.dataobject.storeorder.StoreOrderDO;
|
|
||||||
import co.yixiang.yshop.module.order.service.storeorder.dto.OrderTimeDataDto;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步订单 Service 接口
|
* 异步订单 Service 接口
|
||||||
*
|
*
|
||||||
@@ -16,17 +10,27 @@ public interface AsyncStoreOrderService {
|
|||||||
/**
|
/**
|
||||||
* 计算某个用户的订单统计数据
|
* 计算某个用户的订单统计数据
|
||||||
* @param uid uid>0 取用户 否则取所有
|
* @param uid uid>0 取用户 否则取所有
|
||||||
* @return UserOrderCountVo
|
|
||||||
*/
|
*/
|
||||||
void orderData(Long uid);
|
void orderData(Long uid);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步后台数据统计
|
* 异步后台数据统计
|
||||||
*/
|
*/
|
||||||
void getOrderTimeData();
|
void getOrderTimeData();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付成功后异步打印小票(失败只记日志,不抛异常)
|
||||||
|
*
|
||||||
|
* @param orderId 业务订单号
|
||||||
|
*/
|
||||||
|
void printOrderAsync(String orderId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印小票
|
||||||
|
*
|
||||||
|
* @param orderId 业务订单号
|
||||||
|
* @param force false=自动打印(幂等,校验失败跳过);true=补打(跳过幂等,校验失败抛业务异常)
|
||||||
|
*/
|
||||||
|
void printOrder(String orderId, boolean force);
|
||||||
|
|
||||||
}
|
}
|
||||||
+6
-1
@@ -99,6 +99,11 @@ public interface StoreOrderService {
|
|||||||
*/
|
*/
|
||||||
Long orderNotice();
|
Long orderNotice();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 补打小票(同步,跳过幂等)
|
||||||
|
*
|
||||||
|
* @param id 订单主键
|
||||||
|
*/
|
||||||
|
void printStoreOrder(Long id);
|
||||||
|
|
||||||
}
|
}
|
||||||
+9
@@ -453,6 +453,15 @@ public class StoreOrderServiceImpl implements StoreOrderService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void printStoreOrder(Long id) {
|
||||||
|
StoreOrderDO storeOrderDO = storeOrderMapper.selectById(id);
|
||||||
|
if (storeOrderDO == null) {
|
||||||
|
throw exception(STORE_ORDER_NOT_EXISTS);
|
||||||
|
}
|
||||||
|
asyncStoreOrderService.printOrder(storeOrderDO.getOrderId(), true);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 退回库存
|
* 退回库存
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -170,6 +170,11 @@ yshop:
|
|||||||
pay:
|
pay:
|
||||||
order-notify-url: http://yshop.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
|
order-notify-url: http://yshop.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
|
||||||
refund-notify-url: http://yshop.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
|
refund-notify-url: http://yshop.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
|
||||||
|
xpyun: # 芯烨云打印机 https://www.xpyun.net
|
||||||
|
enabled: true
|
||||||
|
user: neko_chocola@qq.com # 开发者 ID
|
||||||
|
user-key: 36c8251d0ff340888dd0f5fa62aa81ee # 开发者密钥
|
||||||
|
base-url: https://open.xpyun.net/api/openapi
|
||||||
demo: true # 开启演示模式
|
demo: true # 开启演示模式
|
||||||
tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
|
tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ 地图的密钥 https://lbs.qq.com/service/staticV2/staticGuide/staticDoc
|
||||||
file-path: /www/wwwroot/drink/file/
|
file-path: /www/wwwroot/drink/file/
|
||||||
|
|||||||
@@ -206,6 +206,11 @@ yshop:
|
|||||||
pay:
|
pay:
|
||||||
order-notify-url: http://yshop.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
|
order-notify-url: http://yshop.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
|
||||||
refund-notify-url: http://yshop.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
|
refund-notify-url: http://yshop.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
|
||||||
|
xpyun: # 芯烨云打印机 https://www.xpyun.net
|
||||||
|
enabled: true
|
||||||
|
user: neko_chocola@qq.com # 开发者 ID
|
||||||
|
user-key: 36c8251d0ff340888dd0f5fa62aa81ee # 开发者密钥
|
||||||
|
base-url: https://open.xpyun.net/api/openapi
|
||||||
access-log: # 访问日志的配置项
|
access-log: # 访问日志的配置项
|
||||||
enable: false
|
enable: false
|
||||||
demo: false # 关闭演示模式
|
demo: false # 关闭演示模式
|
||||||
|
|||||||
@@ -110,6 +110,11 @@ export const getOrderHtml = async (param1,param2) => {
|
|||||||
return await request.get({ url: `/order/store-order/printOrder?id=` + param1 + `&electId=` + param2})
|
return await request.get({ url: `/order/store-order/printOrder?id=` + param1 + `&electId=` + param2})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 补打小票 */
|
||||||
|
export const printStoreOrder = async (id: number) => {
|
||||||
|
return await request.get({ url: `/order/store-order/printOrder?id=` + id })
|
||||||
|
}
|
||||||
|
|
||||||
export const getShopCount = async () => {
|
export const getShopCount = async () => {
|
||||||
return await request.get({ url: `/order/store-order/count`})
|
return await request.get({ url: `/order/store-order/count`})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,10 @@
|
|||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-dropdown-item v-if = "scope.row.statusStr == '未支付'" @click="handlePay(scope.row.id)">确认付款</el-dropdown-item>
|
<el-dropdown-item v-if = "scope.row.statusStr == '未支付'" @click="handlePay(scope.row.id)">确认付款</el-dropdown-item>
|
||||||
|
<el-dropdown-item
|
||||||
|
v-if="scope.row.statusStr != '未支付' && (scope.row.orderType == 'takein' || scope.row.orderType == 'takeout')"
|
||||||
|
@click="handlePrint(scope.row.id)"
|
||||||
|
>补打</el-dropdown-item>
|
||||||
<el-dropdown-item @click="openForm('orderDetail', scope.row.id)">订单详情</el-dropdown-item>
|
<el-dropdown-item @click="openForm('orderDetail', scope.row.id)">订单详情</el-dropdown-item>
|
||||||
<el-dropdown-item @click="openForm('orderRecord', scope.row.id)">订单记录</el-dropdown-item>
|
<el-dropdown-item @click="openForm('orderRecord', scope.row.id)">订单记录</el-dropdown-item>
|
||||||
<el-dropdown-item @click="handleDelete(scope.row.id)">删除订单</el-dropdown-item>
|
<el-dropdown-item @click="handleDelete(scope.row.id)">删除订单</el-dropdown-item>
|
||||||
@@ -364,6 +368,15 @@ const handlePay = async (id: number) => {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 补打小票 */
|
||||||
|
const handlePrint = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await message.confirm('确认补打该订单小票?')
|
||||||
|
await StoreOrderApi.printStoreOrder(id)
|
||||||
|
message.success('已发送打印')
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
/** 确认收货按钮操作 */
|
/** 确认收货按钮操作 */
|
||||||
const handleTake = async (id: number) => {
|
const handleTake = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -86,6 +86,10 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
<div style="color: red;">绑定管理员用于分店管理</div>
|
<div style="color: red;">绑定管理员用于分店管理</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="打印机SN" prop="uniprintId">
|
||||||
|
<el-input v-model="formData.uniprintId" placeholder="芯烨云打印机设备底部 SN,为空则不自动打印" />
|
||||||
|
<div style="color: #909399; font-size: 12px;">登录芯烨云开放平台绑定打印机后,填入设备 SN</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="是否营业" prop="status">
|
<el-form-item label="是否营业" prop="status">
|
||||||
<el-radio-group v-model="formData.status">
|
<el-radio-group v-model="formData.status">
|
||||||
<el-radio :label="1">是</el-radio>
|
<el-radio :label="1">是</el-radio>
|
||||||
|
|||||||
Reference in new issue
Block a user