阿奇索自动发货接入接口
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled
yudao-ui-admin CI / build (14.x) (push) Has been cancelled
yudao-ui-admin CI / build (16.x) (push) Has been cancelled

This commit is contained in:
sqlicong
2025-08-08 19:38:41 +08:00
parent d2fafdc622
commit 7efbcf0e64
30 changed files with 1342 additions and 320 deletions

View File

@@ -1,50 +0,0 @@
package cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.kfc.service.agiso.AgisoAuthService;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* Agiso开放平台授权回调控制器
* 用于接收授权后返回的code等参数进而换取AccessToken
*/
@RestController
@RequestMapping("/agiso")
public class AgisoAuthCallbackController {
@Resource
private AgisoAuthService agisoAuthService;
/**
* 接收授权回调
* 平台会在用户授权后重定向到该接口并携带code、state等参数
*
* @param code 授权码用于换取AccessToken
* @param state 状态参数(与请求时传入的值一致,用于校验请求合法性)
* @param error 错误信息(授权失败时返回,如用户拒绝授权)
* @return 处理结果
*/
@GetMapping("/callback/authorize")
@Operation(summary = "阿奇索code回调接口")
@PermitAll
public String handleAuthCallback(
@RequestParam(value = "code", required = false) String code,
@RequestParam(value = "state", required = false) String state,
@RequestParam(value = "error", required = false) String error) {
return agisoAuthService.authAndGetAccessToken(code,state,error);
}
@GetMapping("/get/code-url")
@Operation(summary = "生成阿奇索code认证url")
public CommonResult<String> getCodeUrl() {
return success(agisoAuthService.generateAuthUrl());
}
}

View File

@@ -0,0 +1,60 @@
package cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.kfc.service.agiso.AgisoService;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
/**
* Agiso开放平台授权回调控制器
* 用于接收授权后返回的code等参数进而换取AccessToken
*/
@RestController
@RequestMapping("/agiso")
@Slf4j
public class AgisoController {
@Resource
private AgisoService agisoService;
/**
* 接收授权回调
* 平台会在用户授权后重定向到该接口并携带code、state等参数
*
* @param code 授权码用于换取AccessToken
* @param state 状态参数(与请求时传入的值一致,用于校验请求合法性)
* @param error 错误信息(授权失败时返回,如用户拒绝授权)
* @return 处理结果
*/
@GetMapping("/callback/authorize")
@Operation(summary = "阿奇索code回调接口")
@PermitAll
public String handleAuthCallback(@RequestParam(value = "code", required = false) String code,
@RequestParam(value = "state", required = false) String state,
@RequestParam(value = "error", required = false) String error) {
return agisoService.authAndGetAccessToken(code,state,error);
}
@PostMapping("/notify/order-message")
@Operation(summary = "接收下单消息推送接口")
@PermitAll
public String handlePurchaseOrderPush(@RequestParam("fromPlatform") String fromPlatform,
@RequestParam("timestamp") Long timestamp,
@RequestParam("aopic") Integer aopic,
@RequestParam("sign") String sign,
@RequestParam("json") String json) {
log.info("收到采购下单推送消息,平台:{},订单内容:{}", fromPlatform, json);
return agisoService.handlePurchaseOrderPush(fromPlatform, timestamp, aopic, json, sign);
}
@GetMapping("/get/code-url")
@Operation(summary = "生成阿奇索code认证url")
public CommonResult<String> getCodeUrl() {
return success(agisoService.generateAuthUrl());
}
}

View File

@@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.util.List;
/**
* 卡密信息主实体类
*/
@Data
public class AgisoCardInfoRequestVO {
/**
* 子订单编号
* 【必填】
*/
@NotBlank(message = "子订单编号Oid不能为空")
@JsonProperty("Oid")
private Long oid;
/**
* 附言(使用说明)
* 非必填
*/
@JsonProperty("Additional")
private String additional;
/**
* 卡密信息
* 非必填长度不能超过5000字符
*/
@JsonProperty("CardPwdContent")
@Size(max = 5000, message = "卡密信息长度不能超过5000字符")
private List<AgisoCardPwdContentRequestVO> cardPwdContentList;
}

View File

@@ -0,0 +1,58 @@
package cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import java.util.List;
/**
* 卡密内容详情
*/
@Data
public class AgisoCardPwdContentRequestVO {
/**
* 卡号显示类型
* 0文本1条形码2二维码3相册网址4条形码+二维码
*/
@JsonProperty("CardNoShowType")
@Min(value = 0, message = "卡号显示类型必须在0-4之间")
@Max(value = 4, message = "卡号显示类型必须在0-4之间")
private Integer cardNoShowType;
/**
* 卡密显示类型
* 0文本1条形码2二维码3相册网址4条形码+二维码
*/
@JsonProperty("PwdShowType")
@Min(value = 0, message = "卡密显示类型必须在0-4之间")
@Max(value = 4, message = "卡密显示类型必须在0-4之间")
private Integer pwdShowType;
/**
* 卡密标题
*/
@JsonProperty("Title")
private String title;
/**
* 卡号前缀
* 非必填
*/
@JsonProperty("PrefixCardNo")
private String prefixCardNo;
/**
* 卡密前缀
* 非必填
*/
@JsonProperty("PrefixPwd")
private String prefixPwd;
/**
* 卡号密码数组
*/
@JsonProperty("CardPwdArr")
private List<AgisoCardPwdItemRequestVO> cardPwdItemList;
}

View File

@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.time.LocalDateTime;
/**
* 单个卡密项
*/
@Data
public class AgisoCardPwdItemRequestVO {
/**
* 卡号
* 【必填】
*/
@NotBlank(message = "卡号不能为空")
@JsonProperty("c")
private String cardNo;
/**
* 卡密
* 【必填】
*/
@NotBlank(message = "卡密不能为空")
@JsonProperty("p")
private String password;
/**
* 到期时间
* 非必填格式yyyy-MM-dd HH:mm:ss
*/
@JsonProperty("d")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime expireTime;
}

View File

@@ -0,0 +1,104 @@
package cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 阿奇索订单详情完整实体类
* 包含订单主信息及子订单列表
*/
@Data
public class AgisoOrderDetailVO {
/**
* 买家留言
*/
@JsonProperty("BuyerMessage")
private String buyerMessage;
/**
* 买家昵称(可能包含脱敏)
*/
@JsonProperty("BuyerNick")
private String buyerNick;
/**
* 订单创建时间
*/
@JsonProperty("Created")
private String created;
/**
* 商品总数
*/
@JsonProperty("Num")
private Integer num;
/**
* 子订单列表
*/
@JsonProperty("Orders")
private List<AgisoSubOrderVO> orders;
/**
* 扩展卡扩展价格使用金额
*/
@JsonProperty("ExpandCardExpandPriceUsed")
private String expandCardExpandPriceUsed;
/**
* 实付金额
*/
@JsonProperty("Payment")
private String payment;
/**
* 支付时间
*/
@JsonProperty("PayTime")
private String payTime;
/**
* 商品单价
*/
@JsonProperty("Price")
private String price;
/**
* 收货人手机号(可能包含脱敏)
*/
@JsonProperty("ReceiverMobile")
private String receiverMobile;
/**
* 收货人姓名(可能包含脱敏)
*/
@JsonProperty("ReceiverName")
private String receiverName;
/**
* 卖家昵称(可能包含脱敏)
*/
@JsonProperty("SellerNick")
private String sellerNick;
/**
* 订单状态
*/
@JsonProperty("Status")
private String status;
/**
* 主订单编号
*/
@JsonProperty("Tid")
private Long tid;
/**
* 订单总金额
*/
@JsonProperty("TotalFee")
private String totalFee;
}

View File

@@ -0,0 +1,70 @@
package cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 阿奇索采购订单推送消息实体类
* 对应推送JSON中的订单详情数据
*/
@Data
public class AgisoOrderPushVO {
/**
* 订单编号
* 对应JSON中的"Tid"字段
*/
@JsonProperty("Tid")
private Long tid;
/**
* 订单状态
* 例如WAIT_SELLER_SEND_GOODS等待卖家发货、WAIT_BUYER_CONFIRM_GOODS等待买家确认收货
* 对应JSON中的"Status"字段
*/
@JsonProperty("Status")
private String status;
/**
* 卖家昵称
* 对应JSON中的"SellerNick"字段
*/
@JsonProperty("SellerNick")
private String sellerNick;
/**
* 卖家ID平台唯一标识
* 对应JSON中的"SellerOpenUid"字段
*/
@JsonProperty("SellerOpenUid")
private String sellerOpenUid;
/**
* 买家昵称(可能包含脱敏处理,如"碎**"
* 对应JSON中的"BuyerNick"字段
*/
@JsonProperty("BuyerNick")
private String buyerNick;
/**
* 买家ID平台唯一标识
* 对应JSON中的"BuyerOpenUid"字段
*/
@JsonProperty("BuyerOpenUid")
private String buyerOpenUid;
/**
* 支付金额(字符串形式,保留两位小数)
* 对应JSON中的"Payment"字段
*/
@JsonProperty("Payment")
private String payment;
/**
* 交易类型
* 例如fixed一口价
* 对应JSON中的"Type"字段
*/
@JsonProperty("Type")
private String type;
}

View File

@@ -31,5 +31,8 @@ public class AgisoResponse {
* 详细数据(成功时返回)
*/
@JsonProperty("Data")
private AgisoData data;
private String data;
@JsonProperty("AllowRetry")
private String allowRetry;
}

View File

@@ -0,0 +1,79 @@
package cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 阿奇索子订单信息实体类
* 对应订单详情中的子订单数据
*/
@Data
public class AgisoSubOrderVO {
/**
* 子订单商品数量
*/
@JsonProperty("Num")
private Integer num;
/**
* 商品数字ID
*/
@JsonProperty("NumIid")
private Long numIid;
/**
* 子订单编号
*/
@JsonProperty("Oid")
private Long oid;
/**
* 商家外部商品ID
*/
@JsonProperty("OuterIid")
private String outerIid;
/**
* 子订单实付金额
*/
@JsonProperty("Payment")
private String payment;
/**
* 子订单商品单价
*/
@JsonProperty("Price")
private String price;
/**
* 商品SKU属性名称
*/
@JsonProperty("SkuPropertiesName")
private String skuPropertiesName;
/**
* 商品标题
*/
@JsonProperty("Title")
private String title;
/**
* 子订单总金额
*/
@JsonProperty("TotalFee")
private String totalFee;
/**
* 子订单扩展卡扩展价格使用金额
*/
@JsonProperty("ExpandCardExpandPriceUsedSuborder")
private String expandCardExpandPriceUsedSuborder;
/**
* 定制订单信息
*/
@JsonProperty("Customization")
private String customization;
}

View File

@@ -24,18 +24,24 @@ public class OrderSaveReqVO {
private String orderNo;
@Schema(description = "商户ID关联 merchants 表", requiredMode = Schema.RequiredMode.REQUIRED, example = "7304")
@NotNull(message = "商户ID关联 merchants 表不能为空")
// @NotNull(message = "商户ID关联 merchants 表不能为空")
private Long merchantId;
@Schema(description = "平台ID关联 platforms 表", requiredMode = Schema.RequiredMode.REQUIRED, example = "28720")
@NotNull(message = "平台ID关联 platforms 表不能为空")
private Long platformId;
@Schema(description = "商户开放id")
private String sellerOpenUid;
@Schema(description = "外部订单号,如:淘宝,京东等")
private Long outTradeNo;
@Schema(description = "订单类型")
private Integer orderType;
@Schema(description = "买家ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "19247")
@NotEmpty(message = "买家ID不能为空")
// @NotEmpty(message = "买家ID不能为空")
private String buyerId;
@Schema(description = "买家姓名", example = "赵六")

View File

@@ -29,6 +29,9 @@ public class OrderItemsSaveReqVO {
@Schema(description = "商品规格")
private String productSpec;
@Schema(description = "子订单编号")
private Long oid;
@Schema(description = "购买数量", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "购买数量不能为空")
private Integer quantity;

View File

@@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.kfc.convert;
import cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo.AgisoOrderDetailVO;
import cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo.AgisoOrderPushVO;
import cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo.AgisoSubOrderVO;
import cn.iocoder.yudao.module.kfc.controller.admin.orderitems.vo.OrderItemsSaveReqVO;
import cn.iocoder.yudao.module.kfc.dal.dataobject.order.OrderDO;
import cn.iocoder.yudao.module.kfc.enums.order.OrderStatusEnum;
import cn.iocoder.yudao.module.kfc.enums.platform.PlatformTypeEnum;
import org.mapstruct.Mapper;
import org.mapstruct.ReportingPolicy;
import org.mapstruct.factory.Mappers;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.module.kfc.utils.NoGenerator.generate;
@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.IGNORE // 忽略未映射字段
)
public interface OrderConvert {
OrderConvert INSTANCE = Mappers.getMapper(OrderConvert.class);
default OrderDO convert(AgisoOrderPushVO agisoOrderPushVO, String fromPlatform, AgisoOrderDetailVO orderDetailVO){
OrderDO orderDO = new OrderDO();
orderDO.setId(null)
.setOutTradeNo(agisoOrderPushVO.getTid())
.setActualAmount(BigDecimal.valueOf(Double.parseDouble(agisoOrderPushVO.getPayment())))
.setBuyerName(agisoOrderPushVO.getBuyerNick())
.setSellerOpenUid(agisoOrderPushVO.getSellerOpenUid())
.setOrderNo(generate())
.setBuyerId(agisoOrderPushVO.getBuyerOpenUid())
.setBuyerPhone(orderDetailVO.getReceiverMobile())
.setTotalAmount(BigDecimal.valueOf(Double.parseDouble(orderDetailVO.getTotalFee())))
.setOrderStatus(OrderStatusEnum.UNDELIVERED.getStatus())
.setRemark(orderDetailVO.getBuyerMessage())
.setPaymentTime(LocalDateTime.now());
if (fromPlatform.startsWith("Tb"))
orderDO.setPlatformId(PlatformTypeEnum.TB.getType().longValue());
else orderDO.setPlatformId(PlatformTypeEnum.OTHER.getType().longValue());
return orderDO;
}
default OrderItemsSaveReqVO convert(AgisoSubOrderVO agisoSubOrderVO,Long orderId){
OrderItemsSaveReqVO orderItemsSaveReqVO = new OrderItemsSaveReqVO();
orderItemsSaveReqVO.setId(null)
.setOrderId(orderId)
.setOid(agisoSubOrderVO.getOid())
.setProductId(agisoSubOrderVO.getNumIid())
.setProductName(agisoSubOrderVO.getTitle())
.setQuantity(agisoSubOrderVO.getNum())
.setUnitPrice(BigDecimal.valueOf(Double.parseDouble(agisoSubOrderVO.getPrice())))
.setSubtotal(BigDecimal.valueOf(Double.parseDouble(agisoSubOrderVO.getTotalFee())));
return orderItemsSaveReqVO;
}
}

View File

@@ -34,10 +34,18 @@ public class OrderDO extends BaseDO {
* 订单编号,业务唯一标识
*/
private String orderNo;
/**
* 外部平台订单号 如:淘宝,京东等
*/
private Long outTradeNo;
/**
* 商户ID关联 merchants 表
*/
private Long merchantId;
/**
* 商户openID
*/
private String sellerOpenUid;
/**
* 平台ID关联 platforms 表
*/

View File

@@ -44,6 +44,10 @@ public class OrderItemsDO extends BaseDO {
* 商品规格
*/
private String productSpec;
/**
* 子订单编号
*/
private Long oid;
/**
* 购买数量
*/

View File

@@ -58,4 +58,6 @@ public interface ErrorCodeConstants {
ErrorCode USER_AUTH_FAILED = new ErrorCode(2_001_013_004,"用户授权失败");
ErrorCode INTERFACE_RETURNED_FAILURE = new ErrorCode(2_001_013_005,"接口返回失败,错误码:{},错误信息:{}");
ErrorCode REQUEST_FAILED = new ErrorCode(2_001_013_006,"HTTP请求失败状态码{}");
ErrorCode JSON_READ_FAILED = new ErrorCode(2_001_014_000,"JSON读取异常{}");
}

View File

@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.kfc.enums.order;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* 发货结果-类型
*
* @author kfc
*/
@RequiredArgsConstructor
@Getter
public enum AldsTypeEnum implements ArrayValuable<Integer> {
PAY_SUCCESS(1, "付款后发货结果"),
CONFIRM_RECEIPT(2, "确认收货赠送结果"),
PRAISE(4, "好评后赠送结果");
public static final Integer[] ARRAYS = Arrays.stream(values()).map(AldsTypeEnum::getType).toArray(Integer[]::new);
/**
* 类型值
*/
private final Integer type;
/**
* 类型名
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.kfc.enums.order;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
import java.util.Objects;
/**
* 发货结果-类型
*
* @author kfc
*/
@RequiredArgsConstructor
@Getter
public enum AopicEnum implements ArrayValuable<Integer> {
BUYER_TOOK_A_PICTURE(1, "买家拍下"),
BUYER_PAID(2, "买家付款"),
SELLER_MODIFIES_NOTES(524288, "卖家修改备注"),
AUTO_SHIPMENT_SUCCESSFUL(2048, "自动发货成功"),
SELLER_SHIPPING(1048576, "卖家发货"),
BUYER_CONFIRMS_RECEIPT(4, "买家确认收货"),
BOTH_SIDES_HAVE_COMMENTED(16, "双方已评"),
REFUND_CREATION(256, "退款创建"),
REFUNDS_CLOSED(32768, "退款关闭"),
REFUND_SUCCESSFUL(65536, "退款成功"),
SELLER_AGREES_TO_RETURN_THE_PRODUCT(131072, "卖家同意退货"),
SELLER_REFUSED_TO_RETURN_THE_ITEM(262144, "卖家拒绝退货"),
BUYER_PAID_NEW(2097152, "新版买家付款");
public static final Integer[] ARRAYS = Arrays.stream(values()).map(AopicEnum::getType).toArray(Integer[]::new);
/**
* 类型值
*/
private final Integer type;
/**
* 类型名
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
public static AopicEnum valueOf(Integer type) {
return Arrays.stream(values()).filter(v -> Objects.equals(v.getType(), type)).findFirst().orElse(null);
}
}

View File

@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.kfc.enums.order;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* 发货结果-类型
*
* @author kfc
*/
@RequiredArgsConstructor
@Getter
public enum CardShowTypeEnum implements ArrayValuable<Integer> {
TEXT(0, "文本"),
BAR_CODE(1, "条形码"),
QR_CODE(2, "二维码"),
ALBUM_URL(3, "相册网址"),
BAR_QR(4, "条形码+二维码");
public static final Integer[] ARRAYS = Arrays.stream(values()).map(CardShowTypeEnum::getType).toArray(Integer[]::new);
/**
* 类型值
*/
private final Integer type;
/**
* 类型名
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.kfc.enums.platform;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
@RequiredArgsConstructor
@Getter
public enum PlatformTypeEnum implements ArrayValuable<Integer> {
//阿奇索返回平台类型参数值TbAcs,TbAlds,TbArs,Print,Acpr,PddAlds,AldsIdle,AldsJd,AldsDoudian,AldsKwai,AldsYouzan,AldsWeidian,AldsWxVideoShop,AldsXhs
TB(1, "淘宝"),
JD(2, "京东"),
XY(3, "闲鱼"),
KW(4, "快手"),
DY(5, "抖音"),
OTHER(6, "其他");
public static final Integer[] ARRAYS = Arrays.stream(values()).map(PlatformTypeEnum::getType).toArray(Integer[]::new);
/**
* 类型值
*/
private final Integer type;
/**
* 类型名
*/
private final String name;
@Override
public Integer[] array() {
return ARRAYS;
}
public static PlatformTypeEnum valueOf(Integer type) {
for (PlatformTypeEnum enumInstance : PlatformTypeEnum.values()) {
if (enumInstance.getType().equals(type)) {
return enumInstance;
}
}
return null;
}
}

View File

@@ -19,10 +19,10 @@ import java.util.Random;
public class KFCAutomatedHeadlessOrder {
private static final Logger logger = LoggerFactory.getLogger(KFCAutomatedHeadlessOrder.class);
private WebDriver driver;
private WebDriverWait wait;
private Actions actions;
private Random random;
private final WebDriver driver;
private final WebDriverWait wait;
private final Actions actions;
private final Random random;
// 配置信息
private final String phoneNumber = "18881628029"; // 替换为实际手机号

View File

@@ -1,254 +0,0 @@
package cn.iocoder.yudao.module.kfc.service.agiso;
import cn.hutool.core.util.RandomUtil;
import cn.iocoder.yudao.module.kfc.config.AgisoConfig;
import cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo.AgisoData;
import cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo.AgisoResponse;
import cn.iocoder.yudao.module.kfc.utils.HttpUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.kfc.enums.ErrorCodeConstants.*;
import static software.amazon.awssdk.http.HttpStatusCode.BAD_REQUEST;
@Service
@Slf4j
public class AgisoAuthServiceImpl implements AgisoAuthService {
@Resource
private AgisoConfig agisoConfig;
// Redis中存储state的前缀过期时间10分钟
private static final String STATE_REDIS_KEY_PREFIX = "agiso:auth:state:";
private static final String TOKEN_REDIS_KEY_PREFIX = "agiso:token:";
private static final long STATE_EXPIRE_MINUTES = 10;
private static final String AUTO_SHIPMENTS_URL = "http://gw.api.agiso.com/alds/Trade/AldsProcessTrades";
@Resource
private RedisTemplate<String, String> redisTemplate;
@Resource
private OkHttpClient okHttpClient;
@Resource
private ObjectMapper objectMapper;
/**
* 生成授权链接
*/
@Override
public String generateAuthUrl() {
// 1. 生成随机state
String state = RandomUtil.randomNumbers(4);
// 2. 存储state到Redis用于回调校验
redisTemplate.opsForValue()
.set(STATE_REDIS_KEY_PREFIX + state, "VALID", STATE_EXPIRE_MINUTES, TimeUnit.MINUTES);
// 3. 拼接授权URL
return "https://alds.agiso.com/authorize.aspx?appId=" + agisoConfig.getAppid() + "&state=" + state;
}
/**
* 处理授权回调并获取AccessToken
*/
@Override
public String authAndGetAccessToken(String code, String state, String error) {
// 1. 校验state
String redisKey = STATE_REDIS_KEY_PREFIX + state;
if (!redisTemplate.hasKey(redisKey)) {
log.warn("授权回调失败state校验不通过state={}", state);
throw exception(STATE_NOT_VALID);
}
// 校验通过后删除state防止重复使用
redisTemplate.delete(redisKey);
// 2. 处理授权失败场景
if (error != null) {
log.warn("用户授权失败error={}", error);
throw exception(USER_AUTH_FAILED);
}
// 3. 处理授权成功场景
if (code != null) {
//异步执行,不阻塞回调线程
CompletableFuture.runAsync(() -> {
try {
String accessToken = generateAccessToken(code, state);
log.info("授权成功AccessToken已存储到redis");
} catch (Exception e) {
log.error("换取AccessToken失败code={}", code, e);
}
});
}
return "success";
}
/**
* 用授权码code换取AccessToken
*/
private String generateAccessToken(String code, String state) throws IOException {
// 1. 构建请求URL
HttpUrl url = Objects.requireNonNull(HttpUrl.parse("https://alds.agiso.com/auth/token")).newBuilder()
.addQueryParameter("code", code)
.addQueryParameter("appId", agisoConfig.getAppid())
.addQueryParameter("secret", agisoConfig.getAppsecret())
.addQueryParameter("state", state) // 使用回调的state保持一致
.build();
log.debug("请求AccessToken的URL{}", url);
// 2. 创建请求
Request request = new Request.Builder()
.url(url)
.addHeader("Accept", "application/json")
.build();
// 3. 执行请求
try (Response response = okHttpClient.newCall(request).execute()) {
// 4. 检查HTTP响应状态
if (!response.isSuccessful()) {
throw exception(REQUEST_FAILED, response.code());
}
// 5. 读取响应体
String responseBody = response.body() != null ? response.body().string() : "";
log.debug("获取AccessToken的响应{}", responseBody);
if (responseBody.isEmpty()) {
throw exception(RESPONSE_IS_EMPTY);
}
// 6. 解析响应
AgisoResponse tokenResponse = objectMapper.readValue(responseBody, AgisoResponse.class);
if (!tokenResponse.isSuccess()) {
throw exception(INTERFACE_RETURNED_FAILURE,tokenResponse.getErrorCode(),tokenResponse.getErrorMsg());
}
// 7. 提取Token和相关信息
AgisoData data = tokenResponse.getData();
if (data == null || data.getToken() == null || data.getToken().isEmpty()) {
throw exception(TOKEN_IS_EMPTY);
}
// 8. 缓存Token
String cacheKey = TOKEN_REDIS_KEY_PREFIX + data.getSellerOpenUid();
redisTemplate.opsForValue()
.set(cacheKey, data.getToken(), data.getExpiresIn(), TimeUnit.SECONDS);
log.info("AccessToken缓存成功商家ID{},过期时间:{}秒", data.getSellerOpenUid(), data.getExpiresIn());
return data.getToken();
}
}
@Override
public AgisoResponse execute(String url, String method, Map<String, String> params, String sellerOpenUid) throws NoSuchAlgorithmException {
if (params == null) params = new HashMap<>();
//从redis获取accessToken
String accessToken = redisTemplate.opsForValue().get(TOKEN_REDIS_KEY_PREFIX + sellerOpenUid);
if (accessToken == null) throw exception(TOKEN_AUTH_FAILED);
//构建公共请求参数
long timestamp = System.currentTimeMillis() / 1000;
params.put("timestamp", Long.toString(timestamp));
params.put("sign",sign(params,agisoConfig.getAppsecret()));
//构建公共请求头
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
headers.put("ApiVersion", "1");
//执行请求
return HttpUtils.execute(url,method, headers, params);
}
@Override
public AgisoResponse doPost(String url, Map<String, String> params, String sellerOpenUid) throws NoSuchAlgorithmException {
return execute(url,"POST",params,sellerOpenUid);
}
@Override
public AgisoResponse doGet(String url, Map<String, String> params, String sellerOpenUid) throws NoSuchAlgorithmException {
return execute(url,"GET",params,sellerOpenUid);
}
@Override
public AgisoResponse autoShipment(List<String> tids, String sellerOpenUid) throws NoSuchAlgorithmException {
// 校验参数
if (tids == null || tids.isEmpty()) {
log.warn("自动发货失败订单ID列表为空");
AgisoResponse emptyResponse = new AgisoResponse();
emptyResponse.setSuccess(false);
emptyResponse.setErrorCode(BAD_REQUEST);
emptyResponse.setErrorMsg("订单ID列表不能为空");
return emptyResponse;
}
// 拼接订单ID字符串
String tidsStr = String.join(",", tids);
log.debug("自动发货处理订单ID列表{},商家标识:{}", tidsStr, sellerOpenUid);
// 构建请求参数
Map<String, String> params = new HashMap<>();
params.put("tids", tidsStr);
// 调用POST请求方法
return doPost(AUTO_SHIPMENTS_URL, params, sellerOpenUid);
}
/**
* 阿奇索签名算法
* @param params 业务参数
* @param appSecret appsecret
* @return 签名字符串
* @throws NoSuchAlgorithmException 未找到算法异常
*/
@Override
public String sign(Map<String, String> params, String appSecret) throws NoSuchAlgorithmException {
String[] keys = params.keySet().toArray(new String[0]);
Arrays.sort(keys);
StringBuilder query = new StringBuilder();
query.append(appSecret);
for (String key : keys) {
String value = params.get(key);
query.append(key).append(value);
}
query.append(appSecret);
byte[] md5byte = encryptMD5(query.toString());
return byte2hex(md5byte);
}
// byte数组转成16进制字符串
private String byte2hex(byte[] bytes) {
StringBuilder sign = new StringBuilder();
for (byte aByte : bytes) {
String hex = Integer.toHexString(aByte & 0xFF);
if (hex.length() == 1) {
sign.append("0");
}
sign.append(hex.toLowerCase());
}
return sign.toString();
}
// Md5摘要
private byte[] encryptMD5(String data) throws NoSuchAlgorithmException {
MessageDigest md5 = MessageDigest.getInstance("MD5");
return md5.digest(data.getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -2,24 +2,33 @@ package cn.iocoder.yudao.module.kfc.service.agiso;
import cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo.AgisoResponse;
import javax.validation.constraints.NotEmpty;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
public interface AgisoAuthService {
public interface AgisoService {
String generateAuthUrl();
String authAndGetAccessToken(String code, String state, String error);
AgisoResponse execute(String url, String method, Map<String, String> params, String sellerOpenUid) throws IOException, NoSuchAlgorithmException;
AgisoResponse execute(String url, String method, Map<String, String> params, String sellerOpenUid) throws IOException;
AgisoResponse doPost(String url, Map<String, String> params, String sellerOpenUid) throws NoSuchAlgorithmException;
AgisoResponse doPost(String url, Map<String, String> params, String sellerOpenUid);
AgisoResponse doGet(String url, Map<String, String> params, String sellerOpenUid) throws NoSuchAlgorithmException;
AgisoResponse doGet(String url, Map<String, String> params, String sellerOpenUid);
AgisoResponse autoShipment(List<String> tids, String sellerOpenUid) throws NoSuchAlgorithmException;
AgisoResponse autoShipment(List<String> tids, String sellerOpenUid) ;
AgisoResponse getTradeDetail(@NotEmpty String tid, String sellerOpenUid) ;
AgisoResponse createDeliveryResult(String tid, Integer aldsType, String content, String sellerOpenUid);
String handlePurchaseOrderPush(String fromPlatform, Long timestamp, Integer aopic, String json, String sign);
void sendWWMsg(Long tid, String msg, String sellerOpenUid);
String sign(Map<String, String> params, String appSecret)
throws NoSuchAlgorithmException;

View File

@@ -0,0 +1,513 @@
package cn.iocoder.yudao.module.kfc.service.agiso;
import cn.hutool.core.util.RandomUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.kfc.config.AgisoConfig;
import cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo.*;
import cn.iocoder.yudao.module.kfc.controller.admin.order.vo.OrderSaveReqVO;
import cn.iocoder.yudao.module.kfc.controller.admin.orderitems.vo.OrderItemsSaveReqVO;
import cn.iocoder.yudao.module.kfc.convert.OrderConvert;
import cn.iocoder.yudao.module.kfc.dal.dataobject.order.OrderDO;
import cn.iocoder.yudao.module.kfc.dal.dataobject.orderitems.OrderItemsDO;
import cn.iocoder.yudao.module.kfc.enums.order.AldsTypeEnum;
import cn.iocoder.yudao.module.kfc.enums.order.AopicEnum;
import cn.iocoder.yudao.module.kfc.enums.order.CardShowTypeEnum;
import cn.iocoder.yudao.module.kfc.enums.order.OrderStatusEnum;
import cn.iocoder.yudao.module.kfc.service.order.KFCOrderService;
import cn.iocoder.yudao.module.kfc.service.orderitems.OrderItemsService;
import cn.iocoder.yudao.module.kfc.utils.AgisoHttpUtils;
import cn.iocoder.yudao.module.kfc.utils.AssertUtils;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import javax.validation.constraints.NotEmpty;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.kfc.enums.ErrorCodeConstants.*;
import static software.amazon.awssdk.http.HttpStatusCode.BAD_REQUEST;
@Service
@Slf4j
@Validated
public class AgisoServiceImpl implements AgisoService {
// Redis中存储state的前缀过期时间10分钟
private static final String STATE_REDIS_KEY_PREFIX = "agiso:auth:state:";
private static final String TOKEN_REDIS_KEY_PREFIX = "agiso:token:";
private static final long STATE_EXPIRE_MINUTES = 10;
//自行自动发货url
private static final String AUTO_SHIPMENTS_URL = "http://gw.api.agiso.com/alds/Trade/AldsProcessTrades";
//交易订单详情url
private static final String TRADE_DETAIL_URL = "http://gw.api.agiso.com/alds/Trade/TradeInfo";
//创建发货结果url
private static final String DELIVERY_RESULT_URL = "http://gw.api.agiso.com/alds/Trade/CreateOrderResult";
//发送旺旺消息url
private static final String WW_SEND_URL = "http://gw.api.agiso.com/alds/WwMsg/Send";
//订单状态
private static final String ORDER_WAIT_STATUS = "WAIT_SELLER_SEND_GOODS";
private static final OrderConvert convert =OrderConvert.INSTANCE;
@Resource
private AgisoConfig agisoConfig;
@Resource
private RedisTemplate<String, String> redisTemplate;
@Resource
private OkHttpClient okHttpClient;
@Resource
private ObjectMapper objectMapper;
@Resource
private KFCOrderService kfcOrderService;
@Resource
private OrderItemsService itemsService;
/**
* 生成授权链接
*/
@Override
public String generateAuthUrl() {
// 1. 生成随机state
String state = RandomUtil.randomNumbers(4);
// 2. 存储state到Redis用于回调校验
redisTemplate.opsForValue()
.set(STATE_REDIS_KEY_PREFIX + state, "VALID", STATE_EXPIRE_MINUTES, TimeUnit.MINUTES);
// 3. 拼接授权URL
return "https://alds.agiso.com/authorize.aspx?appId=" + agisoConfig.getAppid() + "&state=" + state;
}
/**
* 处理授权回调并获取AccessToken
*/
@Override
public String authAndGetAccessToken(String code, String state, String error) {
// 1. 校验state
String redisKey = STATE_REDIS_KEY_PREFIX + state;
if (!redisTemplate.hasKey(redisKey)) {
log.warn("授权回调失败state校验不通过state={}", state);
throw exception(STATE_NOT_VALID);
}
// 校验通过后删除state防止重复使用
redisTemplate.delete(redisKey);
// 2. 处理授权失败场景
if (error != null) {
log.warn("用户授权失败error={}", error);
throw exception(USER_AUTH_FAILED);
}
// 3. 处理授权成功场景
if (code != null) {
//异步执行,不阻塞回调线程
CompletableFuture.runAsync(() -> {
try {
String accessToken = generateAccessToken(code, state);
log.info("授权成功AccessToken已存储到redis");
} catch (Exception e) {
log.error("换取AccessToken失败code={}", code, e);
}
});
}
return "success";
}
/**
* 用授权码code换取AccessToken
*/
private String generateAccessToken(String code, String state) throws IOException {
// 1. 构建请求URL
HttpUrl url = Objects.requireNonNull(HttpUrl.parse("https://alds.agiso.com/auth/token")).newBuilder()
.addQueryParameter("code", code)
.addQueryParameter("appId", agisoConfig.getAppid())
.addQueryParameter("secret", agisoConfig.getAppsecret())
.addQueryParameter("state", state) // 使用回调的state保持一致
.build();
log.debug("请求AccessToken的URL{}", url);
// 2. 创建请求
Request request = new Request.Builder()
.url(url)
.addHeader("Accept", "application/json")
.build();
// 3. 执行请求
try (Response response = okHttpClient.newCall(request).execute()) {
// 4. 检查HTTP响应状态
if (!response.isSuccessful()) {
throw exception(REQUEST_FAILED, response.code());
}
// 5. 读取响应体
String responseBody = response.body() != null ? response.body().string() : "";
log.debug("获取AccessToken的响应{}", responseBody);
if (responseBody.isEmpty()) {
throw exception(RESPONSE_IS_EMPTY);
}
// 6. 解析响应
AgisoResponse tokenResponse = objectMapper.readValue(responseBody, AgisoResponse.class);
if (!tokenResponse.isSuccess()) {
throw exception(INTERFACE_RETURNED_FAILURE,tokenResponse.getErrorCode(),tokenResponse.getErrorMsg());
}
// 7. 提取Token和相关信息
String responseData = tokenResponse.getData();
AgisoData data = objectMapper.readValue(responseData, AgisoData.class);
if (data == null || data.getToken() == null || data.getToken().isEmpty()) {
throw exception(TOKEN_IS_EMPTY);
}
// 8. 缓存Token
String cacheKey = TOKEN_REDIS_KEY_PREFIX + data.getSellerOpenUid();
redisTemplate.opsForValue()
.set(cacheKey, data.getToken(), data.getExpiresIn(), TimeUnit.SECONDS);
log.info("AccessToken缓存成功商家ID{},过期时间:{}秒", data.getSellerOpenUid(), data.getExpiresIn());
return data.getToken();
}
}
@Override
public AgisoResponse execute(String url, String method, Map<String, String> params, String sellerOpenUid) {
if (params == null) params = new HashMap<>();
//从redis获取accessToken
String accessToken = redisTemplate.opsForValue().get(TOKEN_REDIS_KEY_PREFIX + sellerOpenUid);
if (accessToken == null) throw exception(TOKEN_AUTH_FAILED);
//构建公共请求参数
long timestamp = System.currentTimeMillis() / 1000;
params.put("timestamp", Long.toString(timestamp));
params.put("sign",sign(params,agisoConfig.getAppsecret()));
//构建公共请求头
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
headers.put("ApiVersion", "1");
//执行请求
return AgisoHttpUtils.execute(url,method, headers, params);
}
@Override
public AgisoResponse doPost(String url, Map<String, String> params, String sellerOpenUid) {
return execute(url,"POST",params,sellerOpenUid);
}
@Override
public AgisoResponse doGet(String url, Map<String, String> params, String sellerOpenUid) {
return execute(url,"GET",params,sellerOpenUid);
}
/**
* 执行自动发货
* @param tids 订单号
* @param sellerOpenUid 商户开放id
* @return 阿奇索统一响应
*/
@Override
public AgisoResponse autoShipment(List<String> tids, String sellerOpenUid) {
// 校验参数
if (tids == null || tids.isEmpty()) {
log.warn("自动发货失败订单ID列表为空");
AgisoResponse emptyResponse = new AgisoResponse();
emptyResponse.setSuccess(false);
emptyResponse.setErrorCode(BAD_REQUEST);
emptyResponse.setErrorMsg("订单ID列表不能为空");
return emptyResponse;
}
// 拼接订单ID字符串
String tidsStr = String.join(",", tids);
log.debug("自动发货处理订单ID列表{},商家标识:{}", tidsStr, sellerOpenUid);
// 构建请求参数
Map<String, String> params = new HashMap<>();
params.put("tids", tidsStr);
// 调用POST请求方法
return doPost(AUTO_SHIPMENTS_URL, params, sellerOpenUid);
}
/**
* 获取订单详情
* @param tid 淘宝订单编号
* @param sellerOpenUid 商户开放id
* @return 统一响应
*/
@Override
public AgisoResponse getTradeDetail(@NotEmpty String tid, String sellerOpenUid) {
Map<String, String> params = new HashMap<>();
params.put("tid", tid);
return doPost(TRADE_DETAIL_URL,params,sellerOpenUid);
}
/**
* 创建发货结果
* @param tid 订单编号
* @param aldsType 发货结果类型
* @param content 发货结果内容
* @param sellerOpenUid 商户开放id
* @return 阿奇索统一响应
*/
@Override
public AgisoResponse createDeliveryResult(String tid, Integer aldsType, String content, String sellerOpenUid) {
Map<String, String> params = new HashMap<>();
params.put("tid", tid);
params.put("aldsType", String.valueOf(aldsType));
params.put("content", content);
return doPost(DELIVERY_RESULT_URL,params,sellerOpenUid);
}
/**
* 处理采购下单等通知的推送消息
* 接收并验证推送内容,完成去重判断后返回处理结果
* @param fromPlatform 平台标识
* @param timestamp 时间戳
* @param aopic 推送类型
* @param json 推送消息内容JSON字符串
* @param sign 签名
* @return 处理结果(成功返回"success",失败返回错误信息)
*/
@Override
public String handlePurchaseOrderPush(String fromPlatform, Long timestamp, Integer aopic, String json, String sign) {
try {
// 1. 校验参数完整性
if (fromPlatform == null || timestamp == null || aopic == null || json == null || sign == null) {
log.warn("推送消息参数不完整fromPlatform={}, timestamp={}, aopic={}", fromPlatform, timestamp, aopic);
return "参数不完整";
}
// 2. 验证签名
boolean signValid = verifyPushSign(json, timestamp, sign);
if (!signValid) {
log.warn("推送消息签名验证失败sign={}, json={}, timestamp={}", sign, json, timestamp);
return "签名无效";
}
AopicEnum aopicEnum = AopicEnum.valueOf(aopic);
switch (aopicEnum) {
case BUYER_PAID:
case BUYER_PAID_NEW:
handleBuyerPaid(fromPlatform, json);
break;
//todo 其他类型待实现
case REFUNDS_CLOSED:
case REFUND_CREATION:
case SELLER_SHIPPING:
case REFUND_SUCCESSFUL:
case BUYER_TOOK_A_PICTURE:
case SELLER_MODIFIES_NOTES:
case BUYER_CONFIRMS_RECEIPT:
case AUTO_SHIPMENT_SUCCESSFUL:
case BOTH_SIDES_HAVE_COMMENTED:
case SELLER_REFUSED_TO_RETURN_THE_ITEM:
case SELLER_AGREES_TO_RETURN_THE_PRODUCT:
default:
break;
}
return "success";
} catch (Exception e) {
log.error("处理采购下单推送消息失败", e);
return "处理失败:" + e.getMessage();
}
}
private void handleBuyerPaid(String fromPlatform, String json) throws JsonProcessingException {
// 3. 解析JSON消息内容
AgisoOrderPushVO agisoOrderPushVO = objectMapper.readValue(json, AgisoOrderPushVO.class);
Long tid = agisoOrderPushVO.getTid(); // 订单编号
String status = agisoOrderPushVO.getStatus(); // 订单状态
String sellerOpenUid = agisoOrderPushVO.getSellerOpenUid();//商家开放id
//3.1异步去存储订单到我们的数据库并推送选择门店消息
CompletableFuture.runAsync(() -> {
String orderNo = saveOrderDetail(fromPlatform, tid, agisoOrderPushVO);
//在这里给客户推送链接选择门店并提交订单
String msg =String.format("下单成功请点击下方链接选择门店https://www.cdsrh.top/kfc/kfc/store?code=[%s]", orderNo);
sendWWMsg(tid, msg, sellerOpenUid);
});
// 4. 去重判断基于Redis存储已处理的订单状态
String redisDedupKey = "agiso:push:dedup:" + tid + ":" + status;
if (redisTemplate.hasKey(redisDedupKey)) {
log.info("推送消息已处理去重Tid={}, Status={}", tid, status);
return;
}
// 5. 业务处理
log.info("处理新的采购下单通知,平台={}, 订单号={}, 状态={}, 内容={}",
fromPlatform, tid, status, json);
// 若状态为"等待卖家发货",异步去执行自动发货逻辑
if (ORDER_WAIT_STATUS.equals(status)) {
CompletableFuture.runAsync(()->{
if (sellerOpenUid != null) {
try {
AgisoResponse agisoResponse = autoShipment(Collections.singletonList(tid.toString()), sellerOpenUid);
if (agisoResponse.isSuccess()) {
//去更新数据库
OrderDO order = kfcOrderService.getOrderByOutTradeNo(tid);
if (order != null) {
order.setOrderStatus(OrderStatusEnum.DELIVERED.getStatus());
kfcOrderService.updateOrder(BeanUtils.toBean(order, OrderSaveReqVO.class));
}
log.info("订单{}自动发货触发成功", tid);
}else log.error("订单{}自动发货触发失败,错误码:{},错误信息:{}", tid, agisoResponse.getErrorCode(), agisoResponse.getErrorMsg());
} catch (Exception e) {
log.error("订单{}自动发货触发失败", tid, e);
}
}
});
}
// 6. 标记为已处理
redisTemplate.opsForValue().set(redisDedupKey, "PROCESSED", 24, TimeUnit.HOURS);
}
@Override
public void sendWWMsg(Long tid, String msg, String sellerOpenUid) {
Map<String, String> params = new HashMap<>();
params.put("tid", tid.toString());
params.put("msg",msg);
doPost(WW_SEND_URL,params, sellerOpenUid);
}
@Deprecated
private void handleDeliveryResult(Long tid, String sellerOpenUid) {
OrderDO order = kfcOrderService.getOrderByOutTradeNo(tid);
AssertUtils.notNull(order,ORDER_NOT_EXISTS);
List<OrderItemsDO> itemsDOS = itemsService.getOrderItemsByOrderId(order.getId());
List<AgisoCardInfoRequestVO> list = new ArrayList<>();
for (OrderItemsDO itemsDO : itemsDOS) {
Long oid = itemsDO.getOid();
AgisoCardInfoRequestVO requestVO = new AgisoCardInfoRequestVO();
List<AgisoCardPwdContentRequestVO> contentRequestVOList = new ArrayList<>();
List<AgisoCardPwdItemRequestVO> itemRequestVOList = new ArrayList<>();
AgisoCardPwdContentRequestVO contentRequestVO = new AgisoCardPwdContentRequestVO();
contentRequestVO.setCardNoShowType(CardShowTypeEnum.TEXT.getType())
.setPwdShowType(CardShowTypeEnum.TEXT.getType())
.setCardPwdItemList(itemRequestVOList)
.setTitle("取餐码");
requestVO.setOid(oid).setCardPwdContentList(contentRequestVOList);
list.add(requestVO);
}
String content = JSONObject.toJSONString(list);
createDeliveryResult(String.valueOf(tid), AldsTypeEnum.PAY_SUCCESS.getType(), content,sellerOpenUid);
}
/**
* 存储订单到数据库
* @param fromPlatform 平台标识
* @param tid 淘宝订单编号
* @param agisoOrderPushVO 阿奇索自动推送订单数据
*/
private String saveOrderDetail(String fromPlatform, Long tid, AgisoOrderPushVO agisoOrderPushVO) {
OrderDO orderDO = kfcOrderService.getOrderByOutTradeNo(tid);
if (orderDO == null) {
AgisoResponse tradeDetail = getTradeDetail(String.valueOf(tid), agisoOrderPushVO.getSellerOpenUid());
if (tradeDetail != null && tradeDetail.isSuccess()) {
String data = tradeDetail.getData();
AgisoOrderDetailVO orderDetailVO;
try {
orderDetailVO = objectMapper.readValue(data, AgisoOrderDetailVO.class);
OrderDO orderConvert =convert.convert(agisoOrderPushVO, fromPlatform,orderDetailVO);
Long orderId = kfcOrderService.createOrder(BeanUtils.toBean(orderConvert, OrderSaveReqVO.class));
List<AgisoSubOrderVO> orders = orderDetailVO.getOrders();
orders.forEach(order -> {
OrderItemsSaveReqVO itemsSaveReqVO = convert.convert(order, orderId);
itemsService.createOrderItems(itemsSaveReqVO);
});
OrderDO order = kfcOrderService.getOrder(orderId);
return order.getOrderNo();
} catch (JsonProcessingException e) {
throw exception(JSON_READ_FAILED,e.getMessage());
}
}
}
return null;
}
/**
* 验证推送消息的签名
* @param json 推送的JSON内容
* @param timestamp 时间戳
* @param sign 待验证的签名
* @return 签名是否有效
*/
private boolean verifyPushSign(String json, Long timestamp, String sign) {
String source = agisoConfig.getAppsecret()
+ "json" + json
+ "timestamp" + timestamp
+ agisoConfig.getAppsecret();
byte[] md5Bytes = encryptMD5(source);
String calculatedSign = byte2hex(md5Bytes);
return calculatedSign.equalsIgnoreCase(sign);
}
/**
* 阿奇索签名算法
* @param params 业务参数
* @param appSecret appsecret
* @return 签名字符串
*/
@Override
public String sign(Map<String, String> params, String appSecret) {
String[] keys = params.keySet().toArray(new String[0]);
Arrays.sort(keys);
StringBuilder query = new StringBuilder();
query.append(appSecret);
for (String key : keys) {
String value = params.get(key);
query.append(key).append(value);
}
query.append(appSecret);
byte[] md5byte = encryptMD5(query.toString());
return byte2hex(md5byte);
}
// byte数组转成16进制字符串
private String byte2hex(byte[] bytes) {
StringBuilder sign = new StringBuilder();
for (byte aByte : bytes) {
String hex = Integer.toHexString(aByte & 0xFF);
if (hex.length() == 1) {
sign.append("0");
}
sign.append(hex.toLowerCase());
}
return sign.toString();
}
// Md5摘要
@SneakyThrows
private byte[] encryptMD5(String data) {
MessageDigest md5 = MessageDigest.getInstance("MD5");
return md5.digest(data.getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -65,4 +65,6 @@ public interface KFCOrderService {
Map<Long, List<OrderDO>> getPaidOrderByDate(LocalDateTime startDate, LocalDateTime endDate);
OrderDO getOrderByNo(String orderNo);
OrderDO getOrderByOutTradeNo(Long tid);
}

View File

@@ -93,4 +93,9 @@ public class KFCOrderServiceImpl implements KFCOrderService {
public OrderDO getOrderByNo(String orderNo) {
return KFCOrderMapper.selectOne(OrderDO::getOrderNo, orderNo);
}
@Override
public OrderDO getOrderByOutTradeNo(Long tid) {
return KFCOrderMapper.selectOne(OrderDO::getOutTradeNo, tid);
}
}

View File

@@ -60,4 +60,5 @@ public interface OrderItemsService {
*/
PageResult<OrderItemsDO> getOrderItemsPage(OrderItemsPageReqVO pageReqVO);
List<OrderItemsDO> getOrderItemsByOrderId(Long orderId);
}

View File

@@ -77,4 +77,8 @@ public class OrderItemsServiceImpl implements OrderItemsService {
return orderItemsMapper.selectPage(pageReqVO);
}
@Override
public List<OrderItemsDO> getOrderItemsByOrderId(Long orderId) {
return orderItemsMapper.selectList(OrderItemsDO::getOrderId, orderId);
}
}

View File

@@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.kfc.utils;
import cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo.AgisoData;
import cn.iocoder.yudao.module.kfc.controller.admin.agisoproxy.vo.AgisoResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
@@ -12,7 +11,7 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
public class HttpUtils {
public class AgisoHttpUtils {
// 单例OkHttpClient实例
private static final OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)

View File

@@ -0,0 +1,81 @@
package cn.iocoder.yudao.module.kfc.utils;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
public class NoGenerator {
private static final Random RANDOM = new Random();
private static final AtomicInteger SEQUENCE = new AtomicInteger(1000);
private static final String DEFAULT_BUSINESS_CODE = "SQ-KFC"; // 默认业务标识
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
// 私有构造函数,防止实例化
private NoGenerator() {}
/**
* 生成默认格式的订单编号:业务标识+日期+随机数
* 例如ORD20230615123456789
*/
public static String generate() {
return generate(DEFAULT_BUSINESS_CODE);
}
/**
* 生成带业务标识的订单编号
* 例如BUS20230615123456789
*/
public static String generate(String businessCode) {
String datePart = formatDate(Instant.now());
String randomPart = String.format("%09d", RANDOM.nextInt(1000000000));
return businessCode + datePart + randomPart;
}
/**
* 生成带业务标识和时间戳的订单编号
* 例如BUS20230615143025123456
*/
public static String generateWithTimestamp(String businessCode) {
String datePart = formatDate(Instant.now());
String timestampPart = String.valueOf(System.currentTimeMillis() % 1000000000);
return businessCode + datePart + String.format("%09d", Long.parseLong(timestampPart));
}
/**
* 生成带业务标识和递增序列的订单编号
* 例如BUS20230615000001
*/
public static synchronized String generateWithSequence(String businessCode) {
String datePart = formatDate(Instant.now());
int seq = SEQUENCE.incrementAndGet();
if (seq > 9999) { // 达到最大值重置
SEQUENCE.set(1000);
seq = 1000;
}
return businessCode + datePart + seq;
}
/**
* 生成基于UUID的订单编号
* 例如550e8400-e29b-41d4-a716-446655440000
*/
public static String generateUUID() {
return java.util.UUID.randomUUID().toString().replace("-", "");
}
/**
* 格式化日期为yyyymmdd格式
*/
private static String formatDate(Instant instant) {
if (instant == null) {
return null;
}
LocalDate localDate = instant.atZone(ZoneId.systemDefault()).toLocalDate();
// 使用预定义的格式化器
return localDate.format(FORMATTER);
}
}

View File

@@ -274,5 +274,5 @@ pf4j:
# 阿奇索的配置
agiso:
appid: appid
appsecret: appsecret
appid: 2025080857174100113
appsecret: euhawmh557x9tdzre4vcpnsyh7nkdrmu