记得上下班打卡 | git大法好,push需谨慎

Commit 52399664 authored by 姜秀龙's avatar 姜秀龙

收钱吧 ai1

parent 1350071c
CREATE TABLE `goblin_sqb_mall_info` (
`mid` BIGINT NOT NULL AUTO_INCREMENT,
`mall_id` VARCHAR(64) NOT NULL COMMENT '商城唯一ID',
`mall_sn` VARCHAR(64) NOT NULL COMMENT '收钱吧商城编号',
`mall_name` VARCHAR(128) NOT NULL COMMENT '商城名称',
`signature` VARCHAR(256) NOT NULL COMMENT '商城密钥',
`store_id` VARCHAR(64) NOT NULL COMMENT '关联我方店铺ID',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '0-禁用 1-启用',
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`mid`),
UNIQUE KEY `uk_mall_sn` (`mall_sn`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收钱吧商城信息';
CREATE TABLE `goblin_sqb_goods_ext` (
`mid` BIGINT NOT NULL AUTO_INCREMENT,
`spu_id` VARCHAR(64) NOT NULL COMMENT '关联 goblin_goods.spu_id',
`sku_id` VARCHAR(64) NOT NULL COMMENT '关联 goblin_goods_sku.sku_id',
`mall_sn` VARCHAR(64) NOT NULL COMMENT '所属收钱吧商城编号',
`sqb_product_id` VARCHAR(64) NOT NULL COMMENT '收钱吧商品ID',
`sqb_product_sn` VARCHAR(64) NOT NULL COMMENT '收钱吧商品编号',
`sqb_sku_id` VARCHAR(64) COMMENT '收钱吧SKU ID',
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`mid`),
UNIQUE KEY `uk_spu_sku` (`spu_id`, `sku_id`),
KEY `idx_mall_sn` (`mall_sn`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收钱吧商品扩展信息';
CREATE TABLE `goblin_sqb_performance_goods` (
`mid` BIGINT NOT NULL AUTO_INCREMENT,
`performances_id` VARCHAR(64) NOT NULL COMMENT '演出ID',
`spu_id` VARCHAR(64) NOT NULL COMMENT '商品ID',
`sku_id` VARCHAR(64) NOT NULL COMMENT 'SKU ID',
`sort` INT NOT NULL DEFAULT 0 COMMENT '排序权重',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '0-禁用 1-启用',
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`mid`),
UNIQUE KEY `uk_perf_sku` (`performances_id`, `sku_id`),
KEY `idx_performances_id` (`performances_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='演出-收钱吧商品关联';
CREATE TABLE `goblin_sqb_order` (
`mid` BIGINT NOT NULL AUTO_INCREMENT,
`order_id` VARCHAR(64) NOT NULL COMMENT '本地订单ID',
`user_id` VARCHAR(64) NOT NULL COMMENT '用户ID',
`performances_id` VARCHAR(64) NOT NULL COMMENT '关联演出ID',
`spu_id` VARCHAR(64) NOT NULL COMMENT '商品ID',
`sku_id` VARCHAR(64) NOT NULL COMMENT 'SKU ID',
`quantity` INT NOT NULL DEFAULT 1,
`amount` BIGINT NOT NULL COMMENT '支付金额(分)',
`sqb_order_sn` VARCHAR(64) COMMENT '收钱吧订单号',
`sqb_order_signature` VARCHAR(256) COMMENT '收钱吧订单签名',
`sqb_acquiring_sn` VARCHAR(64) COMMENT '收钱吧收单号',
`sqb_checkout_items_id` VARCHAR(64) COMMENT '结算明细ID',
`coupon_sn` VARCHAR(64) COMMENT '券码编号',
`coupon_qr_code` VARCHAR(512) COMMENT '核销二维码',
`coupon_expire_time` DATETIME COMMENT '券码过期时间',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-待支付 1-已支付 2-已核销 3-已退款 4-退款中 9-失败',
`refund_reason` VARCHAR(256) COMMENT '退款原因',
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`mid`),
UNIQUE KEY `uk_order_id` (`order_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_performances_id` (`performances_id`),
KEY `idx_sqb_acquiring_sn` (`sqb_acquiring_sn`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收钱吧订单';
package com.liquidnet.service.goblin.dto.vo;
import lombok.Data;
import java.io.Serializable;
@Data
public class GoblinSqbCouponVo implements Serializable {
private static final long serialVersionUID = 1L;
private String couponSn;
private String couponQrCode;
private String couponExpireTime;
}
package com.liquidnet.service.goblin.dto.vo;
import lombok.Data;
import java.io.Serializable;
@Data
public class GoblinSqbOrderCreateVo implements Serializable {
private static final long serialVersionUID = 1L;
private String orderId;
private String acquiringSn;
// paymentVoucher fields
private String timeStamp;
private String packageStr;
private String paySign;
private String appId;
private String signType;
private String nonceStr;
}
package com.liquidnet.service.goblin.dto.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 收钱吧订单列表/详情展示 VO
* 融合:GoblinStoreOrderVo(基础信息)+ GoblinOrderSkuVo(商品信息)+ GoblinSqbOrderVo(收钱吧扩展)
*/
@Data
public class GoblinSqbOrderDetailVo implements Serializable {
private static final long serialVersionUID = 1L;
// ========== 来自 GoblinStoreOrderVo(现有订单体系) ==========
@ApiModelProperty(value = "本地订单ID")
private String orderId;
@ApiModelProperty(value = "订单号")
private String orderCode;
@ApiModelProperty(value = "goblin订单状态[0-待付款|2-已付款|5-取消]")
private Integer status;
@ApiModelProperty(value = "实付金额")
private BigDecimal priceActual;
@ApiModelProperty(value = "创建时间")
private String createdAt;
@ApiModelProperty(value = "支付时间")
private String payTime;
// ========== 来自 GoblinOrderSkuVo(sku商品信息) ==========
@ApiModelProperty(value = "商品SPU ID")
private String spuId;
@ApiModelProperty(value = "商品名称")
private String spuName;
@ApiModelProperty(value = "商品SKU ID")
private String skuId;
@ApiModelProperty(value = "款式名称")
private String skuName;
@ApiModelProperty(value = "款式图片")
private String skuImage;
@ApiModelProperty(value = "购买数量")
private Integer quantity;
// ========== 来自 GoblinSqbOrderVo(收钱吧扩展字段) ==========
@ApiModelProperty(value = "收钱吧订单状态:0-待支付 1-已支付 2-已核销 3-已退款 4-退款中 9-失败")
private Integer sqbStatus;
@ApiModelProperty(value = "关联演出ID")
private String performancesId;
@ApiModelProperty(value = "收钱吧收单号")
private String sqbAcquiringSn;
@ApiModelProperty(value = "券码编号")
private String couponSn;
@ApiModelProperty(value = "核销二维码")
private String couponQrCode;
@ApiModelProperty(value = "券码过期时间")
private String couponExpireTime;
@ApiModelProperty(value = "核销状态:0-未核销 1-已核销")
private Integer couponUsedStatus;
}
package com.liquidnet.service.goblin.dto.vo;
import lombok.Data;
import java.io.Serializable;
@Data
public class GoblinSqbOrderVo implements Serializable {
private static final long serialVersionUID = 1L;
private String orderId;
private String userId;
private String performancesId;
private String spuId;
private String skuId;
private Integer quantity;
private Long amount;
private String sqbOrderSn;
private String sqbOrderSignature;
private String sqbAcquiringSn;
private String sqbCheckoutItemsId;
private String couponSn;
private String couponQrCode;
private String couponExpireTime;
private Integer status; // 0-待支付 1-已支付 2-已核销 3-已退款 4-退款中 9-失败
private Integer couponUsedStatus; // 0-未核销 1-已核销
private String refundReason;
private String createdAt;
private String updatedAt;
}
package com.liquidnet.service.goblin.dto.vo;
import lombok.Data;
import java.io.Serializable;
@Data
public class GoblinSqbPerfGoodsVo implements Serializable {
private static final long serialVersionUID = 1L;
private String spuId;
private String spuName;
private String skuId;
private String skuName;
private Long price;
private String coverPic;
private Integer sort;
}
package com.liquidnet.service.goblin.service;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbPerfGoodsVo;
import java.util.List;
import java.util.Map;
/**
* 收钱吧商品同步服务接口
*/
public interface IGoblinSqbGoodsService {
/**
* 获取所有商城及商品列表
* 自动拉取所有商城,循环获取每个商城的商品列表,聚合返回
*
* @return 商城商品列表
*/
ResponseDto<List<GoblinSqbPerfGoodsVo>> getAllMallProducts();
/**
* 批量同步商品到 goblin 系统
* 已存在则更新,不存在则新增(spuType=33)
*
* @param items 待同步商品列表,每项包含 mallSn、sqbProductId、sqbProductSn
* @return 同步结果
*/
ResponseDto<String> syncGoods(List<Map<String, String>> items);
/**
* 查询演出关联商品列表(先查 Redis 缓存,未命中则查 MySQL 并写入缓存)
*
* @param performancesId 演出ID
* @return 演出关联商品列表
*/
ResponseDto<List<GoblinSqbPerfGoodsVo>> getPerfGoods(String performancesId);
}
package com.liquidnet.service.goblin.service;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbCouponVo;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbOrderCreateVo;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbOrderDetailVo;
import java.util.List;
import java.util.Map;
/**
* 收钱吧订单服务接口
*/
public interface IGoblinSqbOrderService {
/**
* 创建收钱吧订单
*
* @param userId 用户ID(从 token 获取)
* @param spuId 商品 SPU ID
* @param skuId 商品 SKU ID
* @param quantity 购买数量
* @param performancesId 关联演出 ID
* @return 订单创建结果(orderId、acquiringSn、paymentVoucher)
*/
ResponseDto<GoblinSqbOrderCreateVo> createOrder(String userId, String spuId, String skuId,
Integer quantity, String performancesId);
/**
* 查询支付状态
*
* @param userId 用户ID
* @param orderId 本地订单ID
* @return 支付状态(0-待支付 1-已支付 9-失败)
*/
ResponseDto<Integer> queryPayStatus(String userId, String orderId);
/**
* 获取核销二维码(券码)
*
* @param userId 用户ID
* @param orderId 本地订单ID
* @return 券码信息(couponSn、couponQrCode、couponExpireTime)
*/
ResponseDto<GoblinSqbCouponVo> queryCoupon(String userId, String orderId);
/**
* 申请退款
*
* @param userId 用户ID
* @param orderId 本地订单ID
* @return 退款结果
*/
ResponseDto<Boolean> refund(String userId, String orderId);
/**
* 主动同步核销状态
*
* @param userId 用户ID
* @param orderId 本地订单ID
* @return 同步结果
*/
ResponseDto<Boolean> syncCouponStatus(String userId, String orderId);
/**
* 支付成功回调(收钱吧主动推送)
*
* @param params 回调参数
* @return "success"
*/
ResponseDto<String> handlePayCallback(Map<String, Object> params);
/**
* 退款成功回调(收钱吧主动推送)
*
* @param params 回调参数
* @return "success"
*/
ResponseDto<String> handleRefundCallback(Map<String, Object> params);
/**
* 券状态变更回调(收钱吧主动推送)
*
* @param params 回调参数
* @return "success"
*/
ResponseDto<String> handleCouponCallback(Map<String, Object> params);
/**
* 演出结束自动退款(定时任务调用)
*
* @param performancesId 演出ID
* @return 处理结果摘要(成功/失败笔数)
*/
ResponseDto<String> autoRefundByPerformance(String performancesId);
/**
* 查询用户收钱吧订单列表
*
* @param userId 用户ID
* @return 收钱吧订单列表(仅 skuType=33 的订单,按下单时间倒序)
*/
ResponseDto<List<GoblinSqbOrderDetailVo>> getOrderList(String userId);
/**
* 查询收钱吧订单详情
*
* @param userId 用户ID
* @param orderId 本地订单ID
* @return 订单详情(基础信息 + 收钱吧扩展信息)
*/
ResponseDto<GoblinSqbOrderDetailVo> getOrderDetail(String userId, String orderId);
/**
* 再次付款(待支付状态下重新拉起微信支付)
*
* @param userId 用户ID
* @param orderId 本地订单ID
* @return 新的 paymentVoucher(复用原 orderId)
*/
ResponseDto<GoblinSqbOrderCreateVo> repay(String userId, String orderId);
}
package com.liquidnet.client.admin.web.controller.zhengzai.goblin;
import com.liquidnet.client.admin.common.core.controller.BaseController;
import com.liquidnet.client.admin.zhengzai.goblin.service.ISqbPerformanceGoodsService;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbPerfGoodsVo;
import io.swagger.annotations.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 演出-收钱吧商品关联管理接口
*/
@Slf4j
@RestController
@Api(tags = "收钱吧-演出商品关联管理")
@RequestMapping("sqb/performance/goods")
public class SqbPerformanceGoodsController extends BaseController {
@Autowired
private ISqbPerformanceGoodsService sqbPerformanceGoodsService;
@PostMapping("bind")
@ApiOperation("关联演出与商品")
@ApiResponse(code = 200, message = "接口返回对象参数")
@ApiImplicitParams({
@ApiImplicitParam(type = "form", required = true, dataType = "String", name = "performancesId", value = "演出ID"),
@ApiImplicitParam(type = "form", required = true, dataType = "String", name = "skuIds", value = "SKU ID列表(多个逗号分隔)"),
@ApiImplicitParam(type = "form", required = false, dataType = "Integer", name = "sort", value = "排序权重", example = "0"),
})
public ResponseDto<Boolean> bind(@RequestParam("performancesId") String performancesId,
@RequestParam("skuIds") List<String> skuIds,
@RequestParam(value = "sort", required = false, defaultValue = "0") Integer sort) {
return sqbPerformanceGoodsService.bind(performancesId, skuIds, sort);
}
@DeleteMapping("unbind")
@ApiOperation("解除演出与商品关联")
@ApiResponse(code = 200, message = "接口返回对象参数")
@ApiImplicitParams({
@ApiImplicitParam(type = "form", required = true, dataType = "String", name = "performancesId", value = "演出ID"),
@ApiImplicitParam(type = "form", required = true, dataType = "String", name = "skuId", value = "SKU ID"),
})
public ResponseDto<Boolean> unbind(@RequestParam("performancesId") String performancesId,
@RequestParam("skuId") String skuId) {
return sqbPerformanceGoodsService.unbind(performancesId, skuId);
}
@GetMapping("list")
@ApiOperation("查询演出关联商品列表(管理后台)")
@ApiResponse(code = 200, message = "接口返回对象参数")
@ApiImplicitParams({
@ApiImplicitParam(type = "query", required = true, dataType = "String", name = "performancesId", value = "演出ID"),
})
public ResponseDto<List<GoblinSqbPerfGoodsVo>> list(@RequestParam("performancesId") String performancesId) {
return sqbPerformanceGoodsService.list(performancesId);
}
}
package com.liquidnet.client.admin.zhengzai.goblin.service;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbPerfGoodsVo;
import java.util.List;
/**
* 演出-收钱吧商品关联 服务接口
*/
public interface ISqbPerformanceGoodsService {
/**
* 关联演出与商品(批量)
*
* @param performancesId 演出ID
* @param skuIds SKU ID 列表
* @param sort 排序权重
* @return 操作结果
*/
ResponseDto<Boolean> bind(String performancesId, List<String> skuIds, Integer sort);
/**
* 解除演出与商品关联
*
* @param performancesId 演出ID
* @param skuId SKU ID
* @return 操作结果
*/
ResponseDto<Boolean> unbind(String performancesId, String skuId);
/**
* 查询演出关联的收钱吧商品列表
*
* @param performancesId 演出ID
* @return 商品列表
*/
ResponseDto<List<GoblinSqbPerfGoodsVo>> list(String performancesId);
}
package com.liquidnet.client.admin.zhengzai.goblin.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.liquidnet.client.admin.zhengzai.goblin.service.ISqbPerformanceGoodsService;
import com.liquidnet.common.cache.redis.util.RedisDataSourceUtil;
import com.liquidnet.commons.lang.util.IDGenerator;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbPerfGoodsVo;
import com.liquidnet.service.goblin.entity.GoblinGoods;
import com.liquidnet.service.goblin.entity.GoblinGoodsSku;
import com.liquidnet.service.goblin.entity.GoblinSqbPerformanceGoods;
import com.liquidnet.service.goblin.mapper.GoblinGoodsMapper;
import com.liquidnet.service.goblin.mapper.GoblinGoodsSkuMapper;
import com.liquidnet.service.goblin.mapper.GoblinSqbPerformanceGoodsMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 演出-收钱吧商品关联 服务实现
* 直连 MySQL,写入/删除时同步删除 Redis 缓存
*/
@Slf4j
@Service
public class SqbPerformanceGoodsServiceImpl implements ISqbPerformanceGoodsService {
/** Redis key 前缀,与 goblin C端接口保持一致 */
private static final String PERF_GOODS_CACHE_KEY_PREFIX = "goblin:sqb:perf:goods:";
@Autowired
private GoblinSqbPerformanceGoodsMapper performanceGoodsMapper;
@Autowired
private GoblinGoodsMapper goblinGoodsMapper;
@Autowired
private GoblinGoodsSkuMapper goblinGoodsSkuMapper;
@Autowired
private RedisDataSourceUtil redisDataSourceUtil;
@Override
public ResponseDto<Boolean> bind(String performancesId, List<String> skuIds, Integer sort) {
if (performancesId == null || performancesId.isEmpty()) {
return ResponseDto.failure("演出ID不能为空");
}
if (skuIds == null || skuIds.isEmpty()) {
return ResponseDto.failure("SKU列表不能为空");
}
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
int sortVal = (sort != null) ? sort : 0;
try {
for (String skuId : skuIds) {
if (skuId == null || skuId.isEmpty()) {
continue;
}
// 查询 SKU 获取 spuId
LambdaQueryWrapper<GoblinGoodsSku> skuQuery = new LambdaQueryWrapper<>();
skuQuery.eq(GoblinGoodsSku::getSkuId, skuId).last("LIMIT 1");
GoblinGoodsSku sku = goblinGoodsSkuMapper.selectOne(skuQuery);
if (sku == null) {
log.warn("[演出商品关联] SKU不存在,skuId={}", skuId);
continue;
}
// 检查是否已存在关联
LambdaQueryWrapper<GoblinSqbPerformanceGoods> existQuery = new LambdaQueryWrapper<>();
existQuery.eq(GoblinSqbPerformanceGoods::getPerformancesId, performancesId)
.eq(GoblinSqbPerformanceGoods::getSkuId, skuId)
.last("LIMIT 1");
GoblinSqbPerformanceGoods existing = performanceGoodsMapper.selectOne(existQuery);
if (existing != null) {
// 已存在则更新 sort 和 status
LambdaUpdateWrapper<GoblinSqbPerformanceGoods> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(GoblinSqbPerformanceGoods::getPerformancesId, performancesId)
.eq(GoblinSqbPerformanceGoods::getSkuId, skuId)
.set(GoblinSqbPerformanceGoods::getSort, sortVal)
.set(GoblinSqbPerformanceGoods::getStatus, 1)
.set(GoblinSqbPerformanceGoods::getUpdatedAt, now);
performanceGoodsMapper.update(null, updateWrapper);
} else {
// 新增关联记录
GoblinSqbPerformanceGoods entity = new GoblinSqbPerformanceGoods();
entity.setPerformancesId(performancesId);
entity.setSpuId(sku.getSpuId());
entity.setSkuId(skuId);
entity.setSort(sortVal);
entity.setStatus(1);
entity.setCreatedAt(now);
entity.setUpdatedAt(now);
performanceGoodsMapper.insert(entity);
}
}
// 删除 Redis 缓存,由 C端接口下次请求时重建
delPerfGoodsCache(performancesId);
return ResponseDto.success(Boolean.TRUE);
} catch (Exception e) {
log.error("[演出商品关联] bind 异常,performancesId={}", performancesId, e);
return ResponseDto.failure("关联操作失败:" + e.getMessage());
}
}
@Override
public ResponseDto<Boolean> unbind(String performancesId, String skuId) {
if (performancesId == null || performancesId.isEmpty()) {
return ResponseDto.failure("演出ID不能为空");
}
if (skuId == null || skuId.isEmpty()) {
return ResponseDto.failure("SKU ID不能为空");
}
try {
LambdaUpdateWrapper<GoblinSqbPerformanceGoods> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(GoblinSqbPerformanceGoods::getPerformancesId, performancesId)
.eq(GoblinSqbPerformanceGoods::getSkuId, skuId)
.set(GoblinSqbPerformanceGoods::getStatus, 0)
.set(GoblinSqbPerformanceGoods::getUpdatedAt,
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
int rows = performanceGoodsMapper.update(null, updateWrapper);
if (rows == 0) {
return ResponseDto.failure("关联记录不存在");
}
// 删除 Redis 缓存
delPerfGoodsCache(performancesId);
return ResponseDto.success(Boolean.TRUE);
} catch (Exception e) {
log.error("[演出商品关联] unbind 异常,performancesId={}, skuId={}", performancesId, skuId, e);
return ResponseDto.failure("解除关联失败:" + e.getMessage());
}
}
@Override
public ResponseDto<List<GoblinSqbPerfGoodsVo>> list(String performancesId) {
if (performancesId == null || performancesId.isEmpty()) {
return ResponseDto.failure("演出ID不能为空");
}
try {
// 查询启用状态的关联记录
LambdaQueryWrapper<GoblinSqbPerformanceGoods> query = new LambdaQueryWrapper<>();
query.eq(GoblinSqbPerformanceGoods::getPerformancesId, performancesId)
.eq(GoblinSqbPerformanceGoods::getStatus, 1)
.orderByAsc(GoblinSqbPerformanceGoods::getSort);
List<GoblinSqbPerformanceGoods> relations = performanceGoodsMapper.selectList(query);
if (relations == null || relations.isEmpty()) {
return ResponseDto.success(new ArrayList<>());
}
// 收集 skuId 和 spuId
List<String> skuIds = relations.stream()
.map(GoblinSqbPerformanceGoods::getSkuId)
.collect(Collectors.toList());
List<String> spuIds = relations.stream()
.map(GoblinSqbPerformanceGoods::getSpuId)
.distinct()
.collect(Collectors.toList());
// 批量查询 SKU 信息
LambdaQueryWrapper<GoblinGoodsSku> skuQuery = new LambdaQueryWrapper<>();
skuQuery.in(GoblinGoodsSku::getSkuId, skuIds);
List<GoblinGoodsSku> skuList = goblinGoodsSkuMapper.selectList(skuQuery);
Map<String, GoblinGoodsSku> skuMap = skuList.stream()
.collect(Collectors.toMap(GoblinGoodsSku::getSkuId, Function.identity(), (a, b) -> a));
// 批量查询 SPU 信息
LambdaQueryWrapper<GoblinGoods> spuQuery = new LambdaQueryWrapper<>();
spuQuery.in(GoblinGoods::getSpuId, spuIds);
List<GoblinGoods> spuList = goblinGoodsMapper.selectList(spuQuery);
Map<String, GoblinGoods> spuMap = spuList.stream()
.collect(Collectors.toMap(GoblinGoods::getSpuId, Function.identity(), (a, b) -> a));
// 组装 VO
List<GoblinSqbPerfGoodsVo> result = new ArrayList<>();
for (GoblinSqbPerformanceGoods rel : relations) {
GoblinSqbPerfGoodsVo vo = new GoblinSqbPerfGoodsVo();
vo.setSkuId(rel.getSkuId());
vo.setSpuId(rel.getSpuId());
vo.setSort(rel.getSort());
GoblinGoodsSku sku = skuMap.get(rel.getSkuId());
if (sku != null) {
vo.setSkuName(sku.getName());
vo.setPrice(sku.getPrice() != null ? sku.getPrice().longValue() : null);
}
GoblinGoods spu = spuMap.get(rel.getSpuId());
if (spu != null) {
vo.setSpuName(spu.getName());
vo.setCoverPic(spu.getCoverPic());
}
result.add(vo);
}
return ResponseDto.success(result);
} catch (Exception e) {
log.error("[演出商品关联] list 异常,performancesId={}", performancesId, e);
return ResponseDto.failure("查询失败:" + e.getMessage());
}
}
/**
* 删除演出关联商品的 Redis 缓存
*/
private void delPerfGoodsCache(String performancesId) {
try {
String cacheKey = PERF_GOODS_CACHE_KEY_PREFIX + performancesId;
redisDataSourceUtil.getRedisGoblinUtil().del(cacheKey);
log.info("[演出商品关联] 已删除 Redis 缓存,key={}", cacheKey);
} catch (Exception e) {
log.warn("[演出商品关联] 删除 Redis 缓存失败,performancesId={}", performancesId, e);
}
}
}
package com.liquidnet.service.goblin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* <p>
* 收钱吧商品扩展信息
* </p>
*
* @author liquidnet
* @since 2025-01-01
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("goblin_sqb_goods_ext")
public class GoblinSqbGoodsExt implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "mid", type = IdType.AUTO)
private Long mid;
/**
* 关联 goblin_goods.spu_id
*/
private String spuId;
/**
* 关联 goblin_goods_sku.sku_id
*/
private String skuId;
/**
* 所属收钱吧商城编号
*/
private String mallSn;
/**
* 收钱吧商品ID
*/
private String sqbProductId;
/**
* 收钱吧商品编号
*/
private String sqbProductSn;
/**
* 收钱吧SKU ID
*/
private String sqbSkuId;
private String createdAt;
private String updatedAt;
}
package com.liquidnet.service.goblin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* <p>
* 收钱吧商城信息
* </p>
*
* @author liquidnet
* @since 2025-01-01
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("goblin_sqb_mall_info")
public class GoblinSqbMallInfo implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "mid", type = IdType.AUTO)
private Long mid;
/**
* 商城唯一ID
*/
private String mallId;
/**
* 收钱吧商城编号
*/
private String mallSn;
/**
* 商城名称
*/
private String mallName;
/**
* 商城密钥
*/
private String signature;
/**
* 关联我方店铺ID
*/
private String storeId;
/**
* 状态 0-禁用 1-启用
*/
private Integer status;
private String createdAt;
private String updatedAt;
}
package com.liquidnet.service.goblin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* <p>
* 收钱吧订单
* </p>
*
* @author liquidnet
* @since 2025-01-01
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("goblin_sqb_order")
public class GoblinSqbOrder implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "mid", type = IdType.AUTO)
private Long mid;
/**
* 本地订单ID
*/
private String orderId;
/**
* 用户ID
*/
private String userId;
/**
* 关联演出ID
*/
private String performancesId;
/**
* 商品ID
*/
private String spuId;
/**
* SKU ID
*/
private String skuId;
/**
* 购买数量
*/
private Integer quantity;
/**
* 支付金额(分)
*/
private Long amount;
/**
* 收钱吧订单号
*/
private String sqbOrderSn;
/**
* 收钱吧订单签名
*/
private String sqbOrderSignature;
/**
* 收钱吧收单号
*/
private String sqbAcquiringSn;
/**
* 结算明细ID
*/
private String sqbCheckoutItemsId;
/**
* 券码编号
*/
private String couponSn;
/**
* 核销二维码
*/
private String couponQrCode;
/**
* 券码过期时间
*/
private String couponExpireTime;
/**
* 状态 0-待支付 1-已支付 2-已核销 3-已退款 4-退款中 9-失败
*/
private Integer status;
/**
* 退款原因
*/
private String refundReason;
private String createdAt;
private String updatedAt;
}
package com.liquidnet.service.goblin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* <p>
* 演出-收钱吧商品关联
* </p>
*
* @author liquidnet
* @since 2025-01-01
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("goblin_sqb_performance_goods")
public class GoblinSqbPerformanceGoods implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "mid", type = IdType.AUTO)
private Long mid;
/**
* 演出ID
*/
private String performancesId;
/**
* 商品ID
*/
private String spuId;
/**
* SKU ID
*/
private String skuId;
/**
* 排序权重
*/
private Integer sort;
/**
* 状态 0-禁用 1-启用
*/
private Integer status;
private String createdAt;
private String updatedAt;
}
package com.liquidnet.service.goblin.mapper;
import com.liquidnet.service.goblin.entity.GoblinSqbGoodsExt;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 收钱吧商品扩展信息 Mapper 接口
* </p>
*
* @author liquidnet
* @since 2025-01-01
*/
public interface GoblinSqbGoodsExtMapper extends BaseMapper<GoblinSqbGoodsExt> {
}
package com.liquidnet.service.goblin.mapper;
import com.liquidnet.service.goblin.entity.GoblinSqbMallInfo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 收钱吧商城信息 Mapper 接口
* </p>
*
* @author liquidnet
* @since 2025-01-01
*/
public interface GoblinSqbMallInfoMapper extends BaseMapper<GoblinSqbMallInfo> {
}
package com.liquidnet.service.goblin.mapper;
import com.liquidnet.service.goblin.entity.GoblinSqbOrder;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 收钱吧订单 Mapper 接口
* </p>
*
* @author liquidnet
* @since 2025-01-01
*/
public interface GoblinSqbOrderMapper extends BaseMapper<GoblinSqbOrder> {
}
package com.liquidnet.service.goblin.mapper;
import com.liquidnet.service.goblin.entity.GoblinSqbPerformanceGoods;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* <p>
* 演出-收钱吧商品关联 Mapper 接口
* </p>
*
* @author liquidnet
* @since 2025-01-01
*/
public interface GoblinSqbPerformanceGoodsMapper extends BaseMapper<GoblinSqbPerformanceGoods> {
}
......@@ -5,6 +5,8 @@ import feign.hystrix.FallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Component
@FeignClient(name = "liquidnet-service-goblin",
......@@ -28,4 +30,7 @@ public interface FeignGoblinTaskClient {
@GetMapping("/rsc/maizhi/job/refundRes")
ResponseDto<Boolean> refundRes();
@PostMapping("/goblin/job/sqb/autoRefund")
ResponseDto<String> sqbAutoRefund(@RequestParam("performancesId") String performancesId);
}
......@@ -68,4 +68,19 @@ public class GoblinTaskHandler {
XxlJobHelper.handleFail();
}
}
/**
* 演出结束自动退款
* xxl-job 配置:JobHandler = sev-goblin:sqbAutoRefund,任务参数传入 performancesId
*/
@XxlJob(value = "sev-goblin:sqbAutoRefund")
public void sqbAutoRefund() {
String performancesId = XxlJobHelper.getJobParam();
try {
XxlJobHelper.handleSuccess("结果:" + feignGoblinTaskClient.sqbAutoRefund(performancesId).getData());
} catch (Exception e) {
XxlJobHelper.log(e);
XxlJobHelper.handleFail();
}
}
}
......@@ -60,6 +60,12 @@
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.liquidnet</groupId>
<artifactId>liquidnet-service-goblin-do</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.liquidnet</groupId>
<artifactId>liquidnet-common-third-zxlnft</artifactId>
......
package com.liquidnet.service.goblin.controller;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.goblin.service.IGoblinSqbOrderService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 收钱吧回调 Controller(收钱吧主动推送,无需鉴权)
*/
@Slf4j
@Api(tags = "收钱吧回调")
@RestController
public class GoblinSqbCallbackController {
@Autowired
private IGoblinSqbOrderService goblinSqbOrderService;
/**
* 支付成功回调
* URL: https://testgoblin.zhengzai.tv/goblin/sqb/order/callback
*/
@PostMapping("/goblin/sqb/order/callback")
@ApiOperation("支付成功回调")
public ResponseDto<String> payCallback(@RequestBody Map<String, Object> params) {
log.info("[收钱吧支付回调] 收到推送");
return goblinSqbOrderService.handlePayCallback(params);
}
/**
* 退款成功回调
* URL: https://testgoblin.zhengzai.tv/goblin/sqb/refund/callback
*/
@PostMapping("/goblin/sqb/refund/callback")
@ApiOperation("退款成功回调")
public ResponseDto<String> refundCallback(@RequestBody Map<String, Object> params) {
log.info("[收钱吧退款回调] 收到推送");
return goblinSqbOrderService.handleRefundCallback(params);
}
/**
* 券状态变更回调
* URL: https://testgoblin.zhengzai.tv/goblin/sqb/coupon/callback
*/
@PostMapping("/goblin/sqb/coupon/callback")
@ApiOperation("券状态变更回调")
public ResponseDto<String> couponCallback(@RequestBody Map<String, Object> params) {
log.info("[收钱吧券状态回调] 收到推送");
return goblinSqbOrderService.handleCouponCallback(params);
}
}
package com.liquidnet.service.goblin.controller;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbPerfGoodsVo;
import com.liquidnet.service.goblin.service.IGoblinSqbGoodsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 收钱吧商品同步 Controller
*/
@Slf4j
@Api(tags = "收钱吧商品同步")
@RestController
@RequestMapping("/goblin/sqb/goods")
public class GoblinSqbGoodsController {
@Autowired
IGoblinSqbGoodsService goblinSqbGoodsService;
/**
* 查询所有商城及商品列表
* 后端自动拉取所有商城,循环获取每个商城的商品列表,聚合返回前端
*/
@GetMapping("/list")
@ApiOperation("查询收钱吧所有商城及商品列表")
public ResponseDto<List<GoblinSqbPerfGoodsVo>> list() {
return goblinSqbGoodsService.getAllMallProducts();
}
/**
* 批量同步商品到 goblin 系统
* 已存在则更新,不存在则新增(spuType=33)
*/
@PostMapping("/sync")
@ApiOperation("批量同步收钱吧商品")
public ResponseDto<String> sync(@RequestBody List<Map<String, String>> items) {
return goblinSqbGoodsService.syncGoods(items);
}
/**
* 查询演出关联商品列表(C端)
* 先查 Redis 缓存(TTL 5min),未命中则查 MySQL 并写入缓存
*/
@GetMapping("/performance/goods/{performancesId}")
@ApiOperation("查询演出关联商品列表")
public ResponseDto<List<GoblinSqbPerfGoodsVo>> getPerfGoods(@PathVariable String performancesId) {
return goblinSqbGoodsService.getPerfGoods(performancesId);
}
}
package com.liquidnet.service.goblin.controller;
import com.liquidnet.commons.lang.util.CurrentUtil;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbCouponVo;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbOrderCreateVo;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbOrderDetailVo;
import com.liquidnet.service.goblin.service.IGoblinSqbOrderService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 收钱吧订单 Controller
*/
@Slf4j
@Api(tags = "收钱吧订单")
@RestController
@RequestMapping("/goblin/sqb/order")
public class GoblinSqbOrderController {
@Autowired
private IGoblinSqbOrderService goblinSqbOrderService;
/**
* 创建收钱吧订单
*/
@PostMapping("/create")
@ApiOperation("创建收钱吧订单")
@ApiImplicitParams({
@ApiImplicitParam(type = "form", required = true, dataType = "String", name = "spuId", value = "商品 SPU ID"),
@ApiImplicitParam(type = "form", required = true, dataType = "String", name = "skuId", value = "商品 SKU ID"),
@ApiImplicitParam(type = "form", required = true, dataType = "Integer", name = "quantity", value = "购买数量", example = "1"),
@ApiImplicitParam(type = "form", required = true, dataType = "String", name = "performancesId", value = "关联演出 ID"),
})
public ResponseDto<GoblinSqbOrderCreateVo> createOrder(
@NotBlank(message = "spuId 不能为空") @RequestParam("spuId") String spuId,
@NotBlank(message = "skuId 不能为空") @RequestParam("skuId") String skuId,
@NotNull(message = "quantity 不能为空")
@Min(value = 1, message = "购买数量至少为 1") @RequestParam("quantity") Integer quantity,
@NotBlank(message = "performancesId 不能为空") @RequestParam("performancesId") String performancesId) {
String userId = CurrentUtil.getCurrentUid();
log.info("[收钱吧下单] 接收请求 userId={}, spuId={}, skuId={}, quantity={}, performancesId={}",
userId, spuId, skuId, quantity, performancesId);
return goblinSqbOrderService.createOrder(userId, spuId, skuId, quantity, performancesId);
}
/**
* 查询支付状态
*/
@GetMapping("/pay/status")
@ApiOperation("查询支付状态")
@ApiImplicitParam(type = "query", required = true, dataType = "String", name = "orderId", value = "本地订单ID")
public ResponseDto<Integer> queryPayStatus(
@NotBlank(message = "orderId 不能为空") @RequestParam("orderId") String orderId) {
String userId = CurrentUtil.getCurrentUid();
log.info("[收钱吧支付状态] 接收请求 userId={}, orderId={}", userId, orderId);
return goblinSqbOrderService.queryPayStatus(userId, orderId);
}
/**
* 获取核销二维码(券码)
*/
@GetMapping("/coupon/{orderId}")
@ApiOperation("获取核销二维码")
@ApiImplicitParam(type = "path", required = true, dataType = "String", name = "orderId", value = "本地订单ID")
public ResponseDto<GoblinSqbCouponVo> queryCoupon(
@NotBlank(message = "orderId 不能为空") @PathVariable("orderId") String orderId) {
String userId = CurrentUtil.getCurrentUid();
log.info("[收钱吧券码] 接收请求 userId={}, orderId={}", userId, orderId);
return goblinSqbOrderService.queryCoupon(userId, orderId);
}
/**
* 申请退款
*/
@PostMapping("/refund")
@ApiOperation("申请退款")
@ApiImplicitParam(type = "form", required = true, dataType = "String", name = "orderId", value = "本地订单ID")
public ResponseDto<Boolean> refund(
@NotBlank(message = "orderId 不能为空") @RequestParam("orderId") String orderId) {
String userId = CurrentUtil.getCurrentUid();
log.info("[收钱吧退款] 接收请求 userId={}, orderId={}", userId, orderId);
return goblinSqbOrderService.refund(userId, orderId);
}
/**
* 主动同步核销状态
*/
@PostMapping("/coupon/sync")
@ApiOperation("主动同步核销状态")
@ApiImplicitParam(type = "form", required = true, dataType = "String", name = "orderId", value = "本地订单ID")
public ResponseDto<Boolean> syncCouponStatus(
@NotBlank(message = "orderId 不能为空") @RequestParam("orderId") String orderId) {
String userId = CurrentUtil.getCurrentUid();
log.info("[收钱吧核销同步] 接收请求 userId={}, orderId={}", userId, orderId);
return goblinSqbOrderService.syncCouponStatus(userId, orderId);
}
/**
* 用户收钱吧订单列表
*/
@GetMapping("/list")
@ApiOperation("用户收钱吧订单列表")
public ResponseDto<java.util.List<GoblinSqbOrderDetailVo>> getOrderList() {
String userId = CurrentUtil.getCurrentUid();
log.info("[收钱吧订单列表] 接收请求 userId={}", userId);
return goblinSqbOrderService.getOrderList(userId);
}
/**
* 订单详情
*/
@GetMapping("/detail/{orderId}")
@ApiOperation("收钱吧订单详情")
@ApiImplicitParam(type = "path", required = true, dataType = "String", name = "orderId", value = "本地订单ID")
public ResponseDto<GoblinSqbOrderDetailVo> getOrderDetail(
@NotBlank(message = "orderId 不能为空") @PathVariable("orderId") String orderId) {
String userId = CurrentUtil.getCurrentUid();
log.info("[收钱吧订单详情] 接收请求 userId={}, orderId={}", userId, orderId);
return goblinSqbOrderService.getOrderDetail(userId, orderId);
}
/**
* 再次付款(待支付状态下重新拉起微信支付)
*/
@PostMapping("/repay")
@ApiOperation("再次付款")
@ApiImplicitParam(type = "form", required = true, dataType = "String", name = "orderId", value = "本地订单ID")
public ResponseDto<GoblinSqbOrderCreateVo> repay(
@NotBlank(message = "orderId 不能为空") @RequestParam("orderId") String orderId) {
String userId = CurrentUtil.getCurrentUid();
log.info("[收钱吧再次付款] 接收请求 userId={}, orderId={}", userId, orderId);
return goblinSqbOrderService.repay(userId, orderId);
}
}
package com.liquidnet.service.goblin.controller.Inner;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.goblin.service.IGoblinSqbOrderService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 收钱吧定时任务入口(供 executor 服务通过 Feign 调用)
*
* xxl-job 配置说明(Task 11.4 运维配置):
* - 在 xxl-job 平台新增定时任务
* - JobHandler:sev-goblin:sqbAutoRefund
* - CRON:按演出结束时间触发(如演出结束后 5 分钟:0 5 * * * ?,具体由运维根据演出时间动态配置)
* - 执行器:liquidnet-service-executor
* - 任务参数:performancesId=xxx(演出ID)
* - 路由策略:第一个
*/
@Slf4j
@Api(tags = "收钱吧定时任务")
@RestController
@RequestMapping("/goblin/job/sqb")
public class GoblinSqbJobController {
@Autowired
private IGoblinSqbOrderService goblinSqbOrderService;
@PostMapping("/autoRefund")
@ApiOperation("演出结束自动退款")
public ResponseDto<String> autoRefund(@RequestParam("performancesId") String performancesId) {
log.info("[收钱吧自动退款] 收到任务,performancesId={}", performancesId);
return goblinSqbOrderService.autoRefundByPerformance(performancesId);
}
}
package com.liquidnet.service.goblin.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.liquidnet.commons.lang.util.CollectionUtil;
import com.liquidnet.commons.lang.util.IDGenerator;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.base.SqlMapping;
import com.liquidnet.service.base.constant.MQConst;
import com.liquidnet.service.goblin.dto.vo.GoblinGoodsInfoVo;
import com.liquidnet.service.goblin.dto.vo.GoblinGoodsSkuInfoVo;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbPerfGoodsVo;
import com.liquidnet.service.goblin.entity.GoblinGoods;
import com.liquidnet.service.goblin.entity.GoblinGoodsSku;
import com.liquidnet.service.goblin.entity.GoblinSqbGoodsExt;
import com.liquidnet.service.goblin.entity.GoblinSqbPerformanceGoods;
import com.liquidnet.service.goblin.mapper.GoblinGoodsMapper;
import com.liquidnet.service.goblin.mapper.GoblinGoodsSkuMapper;
import com.liquidnet.service.goblin.mapper.GoblinSqbGoodsExtMapper;
import com.liquidnet.service.goblin.mapper.GoblinSqbPerformanceGoodsMapper;
import com.liquidnet.service.goblin.param.shouqianba.request.CommonRequest;
import com.liquidnet.service.goblin.param.shouqianba.request.MallListQueryRequest;
import com.liquidnet.service.goblin.param.shouqianba.request.MallProductsQueryRequest;
import com.liquidnet.service.goblin.param.shouqianba.response.data.MallListQueryData;
import com.liquidnet.service.goblin.param.shouqianba.response.data.MallProductsQueryData;
import com.liquidnet.service.goblin.service.IGoblinShouQianBaService;
import com.liquidnet.service.goblin.service.IGoblinSqbGoodsService;
import com.liquidnet.service.goblin.util.GoblinMongoUtils;
import com.liquidnet.service.goblin.util.GoblinRedisUtils;
import com.liquidnet.service.goblin.util.GoblinSqbRedisUtils;
import com.liquidnet.service.goblin.util.QueueUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 收钱吧商品同步服务实现
*/
@Slf4j
@Service
public class GoblinSqbGoodsServiceImpl implements IGoblinSqbGoodsService {
// TODO: 后期移入配置文件
private static final String SQB_APPID = "test_appid";
private static final String SQB_MERCHANT_ID = "todo_merchant_id";
private static final String SQB_MERCHANT_USER_ID = "todo_merchant_user_id";
private static final String SQB_ROLE = "super_admin";
private static final String SQB_STORE_ID = "0"; // 平台级商品 store_id=0
@Autowired
private GoblinSqbGoodsExtMapper goblinSqbGoodsExtMapper;
@Autowired
private IGoblinShouQianBaService goblinShouQianBaService;
@Autowired
private GoblinSqbPerformanceGoodsMapper goblinSqbPerformanceGoodsMapper;
@Autowired
private GoblinGoodsMapper goblinGoodsMapper;
@Autowired
private GoblinGoodsSkuMapper goblinGoodsSkuMapper;
@Autowired
private GoblinSqbRedisUtils goblinSqbRedisUtils;
@Autowired
private GoblinRedisUtils goblinRedisUtils;
@Autowired
private GoblinMongoUtils goblinMongoUtils;
@Autowired
private QueueUtils queueUtils;
// ================================ 获取所有商城商品 ================================
/**
* 获取所有商城及商品列表
* 自动拉取所有商城,循环获取每个商城的商品列表,聚合返回
*/
@Override
public ResponseDto<List<GoblinSqbPerfGoodsVo>> getAllMallProducts() {
List<GoblinSqbPerfGoodsVo> result = new ArrayList<>();
try {
// 1. 调用收钱吧获取商城列表
MallListQueryRequest mallListRequest = new MallListQueryRequest();
mallListRequest.setAppid(SQB_APPID);
// 过滤条件:只查已上线的商城
MallListQueryRequest.Filter filter = new MallListQueryRequest.Filter();
CommonRequest.Seller seller = buildSeller();
filter.setSeller(seller);
filter.setState((byte) 1); // 1=已上线
mallListRequest.setFilter(filter);
// 分页:每次取 100 个
MallListQueryRequest.Cursor cursor = new MallListQueryRequest.Cursor();
cursor.setCount(100);
mallListRequest.setCursor(cursor);
List<MallListQueryData> mallList = goblinShouQianBaService.queryMallList(mallListRequest);
if (CollectionUtils.isEmpty(mallList)) {
log.warn("[收钱吧] 未查询到任何商城信息");
return ResponseDto.success(result);
}
// 2. 循环每个商城获取商品列表
for (MallListQueryData mall : mallList) {
String mallSn = mall.getMallSn();
if (StringUtils.isEmpty(mallSn)) continue;
try {
MallProductsQueryRequest productsRequest = new MallProductsQueryRequest();
productsRequest.setAppid(SQB_APPID);
productsRequest.setSeller(seller);
CommonRequest.Mall mallId = new CommonRequest.Mall();
mallId.setMallSn(mallSn);
mallId.setSignature(mall.getSignature());
productsRequest.setMallID(mallId);
List<MallProductsQueryData> productsDataList = goblinShouQianBaService.queryMallProducts(productsRequest);
if (CollectionUtils.isEmpty(productsDataList)) {
log.warn("[收钱吧商品同步] 商城 {} 获取商品列表为空", mallSn);
continue;
}
// 3. 将商品数据转换为 VO
for (MallProductsQueryData productsData : productsDataList) {
if (productsData == null || CollectionUtils.isEmpty(productsData.getSkuModels())) continue;
String coverPic = null;
if (!CollectionUtils.isEmpty(productsData.getConverImages())) {
coverPic = productsData.getConverImages().get(0);
}
for (MallProductsQueryData.Sku sku : productsData.getSkuModels()) {
GoblinSqbPerfGoodsVo vo = new GoblinSqbPerfGoodsVo();
vo.setSpuId(productsData.getSpuId());
vo.setSpuName(productsData.getTitle());
vo.setSkuId(sku.getSkuId());
String skuName = sku.getSkuName() != null ? sku.getSkuName() : sku.getSkuTitle();
vo.setSkuName(skuName);
vo.setPrice(sku.getPrice());
vo.setCoverPic(coverPic);
result.add(vo);
}
}
} catch (Exception e) {
log.error("[收钱吧商品同步] 获取商城 {} 商品列表异常", mallSn, e);
}
}
} catch (Exception e) {
log.error("[收钱吧商品同步] getAllMallProducts 异常", e);
return ResponseDto.failure("获取商城商品列表失败:" + e.getMessage());
}
log.info("[收钱吧商品同步] getAllMallProducts 完成,共 {} 个 SKU", result.size());
return ResponseDto.success(result);
}
// ================================ 批量同步商品到 goblin 系统 ================================
/**
* 批量同步商品到 goblin 系统
* <p>
* 流程:对每个 sqbProductId,调用收钱吧查询商品详情,
* 写入 goblin_goods + goblin_goods_sku + goblin_sqb_goods_ext
* </p>
*
* @param items 待同步商品列表:每项包含 mallSn、sqbProductId、sqbProductSn(来自收钱吧)
* @return 同步结果摘要
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ResponseDto<String> syncGoods(List<Map<String, String>> items) {
if (CollectionUtils.isEmpty(items)) {
return ResponseDto.failure("同步商品列表不能为空");
}
int newCount = 0;
int existCount = 0;
int failCount = 0;
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
for (Map<String, String> item : items) {
String sqbProductId = item.get("sqbProductId");
String sqbProductSn = item.get("sqbProductSn");
String mallSn = item.get("mallSn");
String mallSignature = item.get("mallSignature"); // 商城签名,用于查商品
if (StringUtils.isEmpty(sqbProductId)) {
log.warn("[收钱吧商品同步] sqbProductId 为空,跳过: {}", item);
failCount++;
continue;
}
try {
// Step 1: 幂等检查 —— 已存在则跳过
LambdaQueryWrapper<GoblinSqbGoodsExt> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(GoblinSqbGoodsExt::getSqbProductId, sqbProductId);
GoblinSqbGoodsExt existing = goblinSqbGoodsExtMapper.selectOne(queryWrapper);
if (existing != null) {
log.info("[收钱吧商品同步] 商品已存在,sqbProductId={}, spuId={}, skuId={}",
sqbProductId, existing.getSpuId(), existing.getSkuId());
existCount++;
continue;
}
// Step 2: 查询收钱吧商品详情(通过 queryMallProducts)
MallProductsQueryData productsData = null;
if (!StringUtils.isEmpty(mallSn)) {
MallProductsQueryRequest productsRequest = new MallProductsQueryRequest();
productsRequest.setAppid(SQB_APPID);
productsRequest.setSeller(buildSeller());
CommonRequest.Mall mallId = new CommonRequest.Mall();
mallId.setMallSn(mallSn);
mallId.setSignature(mallSignature);
productsRequest.setMallID(mallId);
List<MallProductsQueryData> productsDataList = goblinShouQianBaService.queryMallProducts(productsRequest);
if (!CollectionUtils.isEmpty(productsDataList)) {
for (MallProductsQueryData data : productsDataList) {
if (sqbProductId.equals(data.getSpuId())) {
productsData = data;
break;
}
}
}
}
// Step 3: 生成本地 spuId
String spuId = IDGenerator.nextSnowId();
// Step 4: 构建 GoblinGoodsInfoVo
GoblinGoodsInfoVo goodsInfoVo = buildGoblinGoodsInfoVo(spuId, productsData, now);
log.info("[收钱吧商品同步] 准备写入商品,spuId={}", spuId);
// Step 5: 遍历 SKU,构建 GoblinGoodsSkuInfoVo 及写 EXT 表
List<MallProductsQueryData.Sku> sqbSkus = productsData != null && !CollectionUtils.isEmpty(productsData.getSkuModels())
? productsData.getSkuModels() : Collections.emptyList();
List<GoblinGoodsSkuInfoVo> skuInfoVos = new ArrayList<>();
List<String> skuIdList = new ArrayList<>();
if (CollectionUtils.isEmpty(sqbSkus)) {
// 没有 SKU 数据时,创建一个占位 SKU
String skuId = IDGenerator.nextSnowId();
GoblinGoodsSkuInfoVo skuVo = buildGoblinGoodsSkuInfoVo(skuId, spuId, null, productsData, now);
skuInfoVos.add(skuVo);
skuIdList.add(skuId);
GoblinSqbGoodsExt ext = buildExt(spuId, skuId, mallSn, sqbProductId, sqbProductSn, null, now);
goblinSqbGoodsExtMapper.insert(ext);
log.info("[收钱吧商品同步] 写入占位 SKU,skuId={}", skuId);
} else {
for (MallProductsQueryData.Sku sqbSku : sqbSkus) {
String skuId = IDGenerator.nextSnowId();
GoblinGoodsSkuInfoVo skuVo = buildGoblinGoodsSkuInfoVo(skuId, spuId, sqbSku, productsData, now);
skuInfoVos.add(skuVo);
skuIdList.add(skuId);
GoblinSqbGoodsExt ext = buildExt(spuId, skuId, mallSn, sqbProductId, sqbProductSn, sqbSku.getSkuId(), now);
goblinSqbGoodsExtMapper.insert(ext);
log.info("[收钱吧商品同步] 写入 SKU,skuId={}, sqbSkuId={}", skuId, sqbSku.getSkuId());
}
}
goodsInfoVo.setSkuIdList(skuIdList);
// Step 6: 组装价格区间
if (!CollectionUtils.isEmpty(skuInfoVos)) {
long minPrice = skuInfoVos.stream().mapToLong(s -> s.getPrice() != null ? s.getPrice().multiply(BigDecimal.valueOf(100)).longValue() : 0L).min().orElse(0L);
long maxPrice = skuInfoVos.stream().mapToLong(s -> s.getPrice() != null ? s.getPrice().multiply(BigDecimal.valueOf(100)).longValue() : 0L).max().orElse(0L);
goodsInfoVo.setPriceGe(BigDecimal.valueOf(minPrice).divide(BigDecimal.valueOf(100)));
goodsInfoVo.setPriceLe(BigDecimal.valueOf(maxPrice).divide(BigDecimal.valueOf(100)));
goodsInfoVo.setSellPrice(BigDecimal.valueOf(minPrice).divide(BigDecimal.valueOf(100)));
}
// Step 7: 写 MongoDB
goblinMongoUtils.setGoodsInfoVo(goodsInfoVo);
goblinMongoUtils.setGoodsSkuInfoVos(skuInfoVos);
// Step 8: 发队列通知及 Redis 库存写入
LinkedList<String> toMqSqls = CollectionUtil.linkedListString();
LinkedList<Object[]> initGoodsObjs = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> initGoodsSkuObjs = CollectionUtil.linkedListObjectArr();
LinkedList<Object[]> initGoodsImageObjs = CollectionUtil.linkedListObjectArr();
toMqSqls.add(SqlMapping.get("goblin_goods.insert"));
initGoodsObjs.add(new Object[]{
spuId, goodsInfoVo.getSpuNo(), goodsInfoVo.getSpuBarCode(), goodsInfoVo.getSpuErpCode(), goodsInfoVo.getErpType(),
goodsInfoVo.getName(), goodsInfoVo.getSubtitle(), goodsInfoVo.getSellPrice(), goodsInfoVo.getPriceGe(), goodsInfoVo.getPriceLe(),
goodsInfoVo.getIntro(), goodsInfoVo.getDetails(), goodsInfoVo.getCoverPic(), goodsInfoVo.getVideo(), goodsInfoVo.getSpecMode(),
goodsInfoVo.getStoreId(), goodsInfoVo.getCateFid(), goodsInfoVo.getCateSid(), goodsInfoVo.getCateTid(), goodsInfoVo.getStoreCateFid(),
goodsInfoVo.getStoreCateSid(), goodsInfoVo.getStoreCateTid(), goodsInfoVo.getBrandId(), goodsInfoVo.getShelvesHandle(), goodsInfoVo.getShelvesTime(),
goodsInfoVo.getSpuValidity(), goodsInfoVo.getVirtualFlg(), goodsInfoVo.getStatus(), goodsInfoVo.getShelvesStatus(), goodsInfoVo.getSpuAppear(),
goodsInfoVo.getShelvesAt(), goodsInfoVo.getCreatedBy(), goodsInfoVo.getCreatedAt(), goodsInfoVo.getLogisticsTemplate()
});
toMqSqls.add(SqlMapping.get("goblin_goods_sku.insert"));
for (GoblinGoodsSkuInfoVo skuInfoVo : skuInfoVos) {
// 写 Redis 库存
goblinRedisUtils.setSkuStock(null, skuInfoVo.getSkuId(), skuInfoVo.getStock());
initGoodsSkuObjs.add(new Object[]{
skuInfoVo.getSkuId(), skuInfoVo.getSpuId(), skuInfoVo.getSkuNo(), skuInfoVo.getSkuBarCode(), skuInfoVo.getSkuErpCode(),
skuInfoVo.getErpType(), skuInfoVo.getErpHosting(), skuInfoVo.getErpWarehouseNo(), skuInfoVo.getSkuType(), skuInfoVo.getName(),
skuInfoVo.getSubtitle(), skuInfoVo.getSellPrice(), skuInfoVo.getSkuPic(), skuInfoVo.getSkuIsbn(), skuInfoVo.getStock(),
skuInfoVo.getSkuStock(), skuInfoVo.getWarningStock(), skuInfoVo.getPrice(), skuInfoVo.getPriceMember(), skuInfoVo.getWeight(),
skuInfoVo.getBuyFactor(), skuInfoVo.getBuyRoster(), skuInfoVo.getBuyLimit(), skuInfoVo.getStoreId(), skuInfoVo.getSkuValidity(),
skuInfoVo.getVirtualFlg(), skuInfoVo.getStatus(), skuInfoVo.getShelvesStatus(), skuInfoVo.getSkuAppear(), skuInfoVo.getShelvesAt(),
goodsInfoVo.getCreatedBy(), goodsInfoVo.getCreatedAt(), skuInfoVo.getLogisticsTemplate()
});
}
toMqSqls.add(SqlMapping.get("goblin_goods_image.insert_byreplace"));
if (!CollectionUtils.isEmpty(goodsInfoVo.getImageList())) {
goodsInfoVo.getImageList().forEach(imageUrl -> initGoodsImageObjs.add(new Object[]{spuId, imageUrl}));
}
queueUtils.sendMsgByRedis(MQConst.GoblinQueue.SQL_GOODS.getKey(),
SqlMapping.gets(toMqSqls, initGoodsObjs, initGoodsSkuObjs, initGoodsImageObjs));
log.info("[收钱吧商品同步] Redis及MongoDB写入、MQ消息发送成功,sqbProductId={}", sqbProductId);
newCount++;
} catch (Exception e) {
log.error("[收钱吧商品同步] 处理商品异常,sqbProductId={}", sqbProductId, e);
failCount++;
}
}
String summary = String.format("同步完成:新增 %d 条,已存在 %d 条,失败 %d 条", newCount, existCount, failCount);
log.info("[收钱吧商品同步] {}", summary);
return ResponseDto.success(summary);
}
// ================================ 查询演出关联商品 ================================
/**
* 查询演出关联商品列表
* 先查 Redis 缓存(TTL 5min),未命中则查 MySQL 并写入缓存
*/
@Override
public ResponseDto<List<GoblinSqbPerfGoodsVo>> getPerfGoods(String performancesId) {
// 1. 先查 Redis 缓存
List<GoblinSqbPerfGoodsVo> cached = goblinSqbRedisUtils.getPerfGoods(performancesId);
if (cached != null) {
return ResponseDto.success(cached);
}
// 2. 缓存未命中,查 MySQL
try {
LambdaQueryWrapper<GoblinSqbPerformanceGoods> query = new LambdaQueryWrapper<>();
query.eq(GoblinSqbPerformanceGoods::getPerformancesId, performancesId)
.eq(GoblinSqbPerformanceGoods::getStatus, 1)
.orderByAsc(GoblinSqbPerformanceGoods::getSort);
List<GoblinSqbPerformanceGoods> relations = goblinSqbPerformanceGoodsMapper.selectList(query);
if (CollectionUtils.isEmpty(relations)) {
List<GoblinSqbPerfGoodsVo> empty = new ArrayList<>();
goblinSqbRedisUtils.setPerfGoods(performancesId, empty);
return ResponseDto.success(empty);
}
// 收集 skuId 和 spuId
List<String> skuIds = relations.stream()
.map(GoblinSqbPerformanceGoods::getSkuId)
.collect(Collectors.toList());
List<String> spuIds = relations.stream()
.map(GoblinSqbPerformanceGoods::getSpuId)
.distinct()
.collect(Collectors.toList());
// 批量查询 SKU 信息
LambdaQueryWrapper<GoblinGoodsSku> skuQuery = new LambdaQueryWrapper<>();
skuQuery.in(GoblinGoodsSku::getSkuId, skuIds);
List<GoblinGoodsSku> skuList = goblinGoodsSkuMapper.selectList(skuQuery);
Map<String, GoblinGoodsSku> skuMap = skuList.stream()
.collect(Collectors.toMap(GoblinGoodsSku::getSkuId, Function.identity(), (a, b) -> a));
// 批量查询 SPU 信息
LambdaQueryWrapper<GoblinGoods> spuQuery = new LambdaQueryWrapper<>();
spuQuery.in(GoblinGoods::getSpuId, spuIds);
List<GoblinGoods> spuList = goblinGoodsMapper.selectList(spuQuery);
Map<String, GoblinGoods> spuMap = spuList.stream()
.collect(Collectors.toMap(GoblinGoods::getSpuId, Function.identity(), (a, b) -> a));
// 组装 VO
List<GoblinSqbPerfGoodsVo> result = new ArrayList<>();
for (GoblinSqbPerformanceGoods rel : relations) {
GoblinSqbPerfGoodsVo vo = new GoblinSqbPerfGoodsVo();
vo.setSkuId(rel.getSkuId());
vo.setSpuId(rel.getSpuId());
vo.setSort(rel.getSort());
GoblinGoodsSku sku = skuMap.get(rel.getSkuId());
if (sku != null) {
vo.setSkuName(sku.getName());
vo.setPrice(sku.getPrice() != null ? sku.getPrice().longValue() : null);
}
GoblinGoods spu = spuMap.get(rel.getSpuId());
if (spu != null) {
vo.setSpuName(spu.getName());
vo.setCoverPic(spu.getCoverPic());
}
result.add(vo);
}
// 3. 写入 Redis 缓存(TTL 5min)
goblinSqbRedisUtils.setPerfGoods(performancesId, result);
return ResponseDto.success(result);
} catch (Exception e) {
log.error("[收钱吧] getPerfGoods 异常,performancesId={}", performancesId, e);
return ResponseDto.failure("查询演出关联商品失败:" + e.getMessage());
}
}
// ================================ 私有辅助方法 ================================
private CommonRequest.Seller buildSeller() {
CommonRequest.Seller seller = new CommonRequest.Seller();
seller.setMerchantId(SQB_MERCHANT_ID);
seller.setMerchantUserId(SQB_MERCHANT_USER_ID);
seller.setRole(SQB_ROLE);
return seller;
}
/**
* 构建 GoblinGoodsInfoVo(收钱吧商品固定字段)
*/
private GoblinGoodsInfoVo buildGoblinGoodsInfoVo(String spuId, MallProductsQueryData productsData, String now) {
GoblinGoodsInfoVo goods = new GoblinGoodsInfoVo();
goods.setSpuId(spuId);
goods.setSpuNo(spuId);
goods.setSpuType(33); // 33 = 收钱吧商品
goods.setVirtualFlg("1"); // 虚拟商品
goods.setStatus("3"); // 3 = 审核通过(收钱吧商品无需审核)
goods.setShelvesHandle("2"); // 2 = 直接上架售卖
goods.setShelvesStatus("3"); // 3 = 上架
goods.setSpuAppear("0"); // 默认展示
goods.setDelFlg("0"); // 未删除
goods.setStoreId(SQB_STORE_ID);
goods.setCreatedBy("system");
goods.setCreatedAt(LocalDateTime.now());
goods.setUpdatedBy("system");
goods.setUpdatedAt(LocalDateTime.now());
goods.setImageList(new ArrayList<>());
if (productsData != null) {
goods.setName(productsData.getTitle());
goods.setIntro(productsData.getProductIntroduction());
if (!CollectionUtils.isEmpty(productsData.getConverImages())) {
goods.setCoverPic(productsData.getConverImages().get(0));
goods.setImageList(new ArrayList<>(productsData.getConverImages()));
}
} else {
goods.setName("收钱吧商品-" + spuId);
}
goods.setSkuIdList(new ArrayList<>());
return goods;
}
/**
* 构建 GoblinGoodsSkuInfoVo 实体
*/
private GoblinGoodsSkuInfoVo buildGoblinGoodsSkuInfoVo(String skuId, String spuId,
MallProductsQueryData.Sku sqbSku,
MallProductsQueryData productsData,
String now) {
GoblinGoodsSkuInfoVo sku = new GoblinGoodsSkuInfoVo();
sku.setSkuId(skuId);
sku.setSpuId(spuId);
sku.setSkuNo(skuId);
sku.setSkuType(33); // 33 = 收钱吧商品
sku.setVirtualFlg("1"); // 虚拟商品
sku.setStatus("3"); // 审核通过
sku.setShelvesHandle("2"); // 直接上架
sku.setShelvesStatus("3"); // 上架
sku.setSkuAppear("0");
sku.setBuyFactor("0"); // 全部用户可买
sku.setBuyLimit(0); // 不限购
sku.setDelFlg("0");
sku.setStoreId(SQB_STORE_ID);
sku.setCreatedBy("system");
sku.setCreatedAt(LocalDateTime.now());
sku.setUpdatedBy("system");
sku.setUpdatedAt(LocalDateTime.now());
if (sqbSku != null) {
// SKU 名称
String skuName = sqbSku.getSkuName() != null ? sqbSku.getSkuName() : sqbSku.getSkuTitle();
sku.setName(skuName != null ? skuName : (productsData != null ? productsData.getTitle() : "规格"));
// 收钱吧价格单位为分,转元
if (sqbSku.getPrice() != null) {
BigDecimal priceYuan = BigDecimal.valueOf(sqbSku.getPrice()).divide(BigDecimal.valueOf(100));
sku.setPrice(priceYuan);
sku.setSellPrice(priceYuan);
}
// 库存:优先取 quantity,若为 null 则默认 9999
int stock = sqbSku.getQuantity() != null ? sqbSku.getQuantity().intValue() : 9999;
sku.setStock(stock);
sku.setSkuStock(stock);
if (productsData != null && !CollectionUtils.isEmpty(productsData.getConverImages())) {
sku.setSkuPic(productsData.getConverImages().get(0));
}
} else {
sku.setName(productsData != null ? productsData.getTitle() : "规格");
sku.setPrice(BigDecimal.ZERO);
sku.setSellPrice(BigDecimal.ZERO);
sku.setStock(0);
sku.setSkuStock(0);
}
return sku;
}
/**
* 构建 GoblinSqbGoodsExt 实体
*/
private GoblinSqbGoodsExt buildExt(String spuId, String skuId, String mallSn,
String sqbProductId, String sqbProductSn,
String sqbSkuId, String now) {
GoblinSqbGoodsExt ext = new GoblinSqbGoodsExt();
ext.setSpuId(spuId);
ext.setSkuId(skuId);
ext.setMallSn(mallSn);
ext.setSqbProductId(sqbProductId);
ext.setSqbProductSn(sqbProductSn);
ext.setSqbSkuId(sqbSkuId);
ext.setCreatedAt(now);
ext.setUpdatedAt(now);
return ext;
}
}
package com.liquidnet.service.goblin.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.liquidnet.commons.lang.util.IDGenerator;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.goblin.dto.vo.*;
import com.liquidnet.service.goblin.entity.GoblinSqbPerformanceGoods;
import com.liquidnet.service.goblin.mapper.GoblinSqbPerformanceGoodsMapper;
import com.liquidnet.service.goblin.param.shouqianba.request.*;
import com.liquidnet.service.goblin.param.shouqianba.response.data.*;
import com.liquidnet.service.goblin.service.IGoblinShouQianBaService;
import com.liquidnet.service.goblin.service.IGoblinSqbOrderService;
import com.liquidnet.service.goblin.util.GoblinRedisUtils;
import com.liquidnet.service.goblin.util.GoblinSqbRedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
* 收钱吧订单服务实现
*/
@Slf4j
@Service
public class GoblinSqbOrderServiceImpl implements IGoblinSqbOrderService {
@Autowired
private GoblinSqbRedisUtils goblinSqbRedisUtils;
@Autowired
private GoblinRedisUtils goblinRedisUtils;
@Autowired
private com.liquidnet.service.goblin.util.GoblinMongoUtils goblinMongoUtils;
@Autowired
private com.liquidnet.service.goblin.util.QueueUtils queueUtils;
@Autowired
private GoblinSqbPerformanceGoodsMapper goblinSqbPerformanceGoodsMapper;
@Autowired
private IGoblinShouQianBaService goblinShouQianBaService;
private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// ========== 常量(后期可移至配置文件) ==========
// TODO: 将以下常量移至 application.yml 配置
private static final String SQB_APPID = "test_appid";
private static final String SQB_MERCHANT_ID = "todo_merchant_id";
private static final String SQB_MERCHANT_USER_ID = "todo_merchant_user_id";
private static final String SQB_ROLE = "super_admin";
// ================================ 创建订单 ================================
@Override
public ResponseDto<GoblinSqbOrderCreateVo> createOrder(String userId, String spuId, String skuId,
Integer quantity, String performancesId) {
log.info("[收钱吧下单] 开始 userId={}, spuId={}, skuId={}, quantity={}, performancesId={}",
userId, spuId, skuId, quantity, performancesId);
// Step 1: 分布式锁防重
boolean locked = goblinSqbRedisUtils.tryOrderLock(userId, skuId);
if (!locked) {
log.warn("[收钱吧下单] 获取防重锁失败,userId={}, skuId={}", userId, skuId);
return ResponseDto.failure("请勿重复下单");
}
try {
// Step 2: 校验演出-商品关联
LambdaQueryWrapper<GoblinSqbPerformanceGoods> query = new LambdaQueryWrapper<>();
query.eq(GoblinSqbPerformanceGoods::getPerformancesId, performancesId)
.eq(GoblinSqbPerformanceGoods::getSkuId, skuId)
.eq(GoblinSqbPerformanceGoods::getStatus, 1);
GoblinSqbPerformanceGoods relation = goblinSqbPerformanceGoodsMapper.selectOne(query);
if (relation == null) {
log.warn("[收钱吧下单] 演出-商品关联不存在或已禁用,performancesId={}, skuId={}", performancesId, skuId);
return ResponseDto.failure("商品与演出关联不存在");
}
// Step 3: 原子扣减库存
long remaining = goblinRedisUtils.decrSkuStock(null, skuId, quantity);
if (remaining < 0) {
goblinRedisUtils.incrSkuStock(null, skuId, quantity);
log.warn("[收钱吧下单] 库存不足,skuId={}, quantity={}", skuId, quantity);
return ResponseDto.failure("库存不足");
}
log.info("[收钱吧下单] 扣减库存成功,skuId={}, remaining={}", skuId, remaining);
// Step 4: 构建收钱吧公共请求参数
CommonRequest.Seller seller = buildSeller();
// Step 4.1: createSettlement → 得 checkoutItemsId
// TODO: 从商品数据中获取价格、图片、标题等信息填充 CheckoutItem
SettlementCreateRequest settlementReq = new SettlementCreateRequest();
settlementReq.setAppid(SQB_APPID);
settlementReq.setSeller(seller);
CommonRequest.Buyer buyer = new CommonRequest.Buyer();
buyer.setBuyerId(userId);
settlementReq.setBuyer(buyer);
// TODO: settlementReq.setCheckoutItems(...); settlementReq.setAmount(...);
SettlementCreateData settlementData = goblinShouQianBaService.createSettlement(settlementReq);
if (settlementData == null) {
goblinRedisUtils.incrSkuStock(null, skuId, quantity);
return ResponseDto.failure("创建结算明细失败");
}
String checkoutItemsId = settlementData.getCheckoutItemsId();
log.info("[收钱吧下单] createSettlement 成功,checkoutItemsId={}", checkoutItemsId);
// Step 4.2: createOrder → 得 sqbOrderSn + sqbOrderSignature + sqbAcquiringSn
OrderCreateRequest orderReq = new OrderCreateRequest();
orderReq.setAppid(SQB_APPID);
orderReq.setSeller(seller);
orderReq.setCheckoutItemsId(checkoutItemsId);
orderReq.setBuyer(buyer);
orderReq.setRequestId(IDGenerator.nextSnowId());
orderReq.setSubject("收钱吧商品");
OrderCreateData orderData = goblinShouQianBaService.createOrder(orderReq);
if (orderData == null) {
goblinRedisUtils.incrSkuStock(null, skuId, quantity);
return ResponseDto.failure("创建收钱吧订单失败");
}
String sqbOrderSn = orderData.getOrderSn();
String sqbOrderSignature = orderData.getOrderSignature();
String sqbAcquiringSn = orderData.getAcquiring() != null ? orderData.getAcquiring().getAcquiringSn() : null;
log.info("[收钱吧下单] createOrder 成功,sqbOrderSn={}, sqbAcquiringSn={}", sqbOrderSn, sqbAcquiringSn);
// Step 4.3: queryCashier → 得 selectedSignature + seq
CashierQueryRequest cashierReq = buildCashierQueryRequest(seller, sqbAcquiringSn, sqbOrderSignature);
CashierQueryData cashierData = goblinShouQianBaService.queryCashier(cashierReq);
if (cashierData == null) {
goblinRedisUtils.incrSkuStock(null, skuId, quantity);
return ResponseDto.failure("查询收银台失败");
}
String selectedSignature = cashierData.getSelectedSignature();
String seq = cashierData.getSeq();
log.info("[收钱吧下单] queryCashier 成功,selectedSignature={}, seq={}", selectedSignature, seq);
// Step 4.4: createWechatPrepayOrder → 得 paymentVoucher
CreateWechatPrepayOrderRequest prepayReq = buildWechatPrepayRequest(
seller, sqbAcquiringSn, sqbOrderSignature, selectedSignature, seq, cashierData);
CreateWechatPrepayOrderData prepayData = goblinShouQianBaService.createWechatPrepayOrder(prepayReq);
if (prepayData == null || prepayData.getPaymentVoucher() == null) {
goblinRedisUtils.incrSkuStock(null, skuId, quantity);
return ResponseDto.failure("创建微信预支付失败");
}
CreateWechatPrepayOrderData.PaymentVoucher pv = prepayData.getPaymentVoucher();
log.info("[收钱吧下单] createWechatPrepayOrder 成功,timeStamp={}", pv.getTimeStamp());
String orderId = IDGenerator.nextSnowId();
String masterOrderCode = IDGenerator.nextTimeId();
String orderCode = IDGenerator.nextTimeId();
String now = LocalDateTime.now().format(DTF);
// 获取商品和 SKU 信息,供后续订单基础信息拼接使用
GoblinGoodsInfoVo goodsInfo = goblinMongoUtils.getGoodsInfoVo(spuId);
GoblinGoodsSkuInfoVo skuInfo = goblinMongoUtils.getGoodsSkuInfoVo(skuId);
// 1) 构建基础商城订单 VO (GoblinStoreOrderVo)
GoblinStoreOrderVo storeOrderVo = new GoblinStoreOrderVo();
storeOrderVo.setMasterOrderCode(masterOrderCode);
storeOrderVo.setOrderId(orderId);
storeOrderVo.setStoreId(goodsInfo != null ? goodsInfo.getStoreId() : "SQB_STORE");
storeOrderVo.setStoreName(goodsInfo != null ? goodsInfo.getStoreName() : "收钱吧商品聚合门店");
storeOrderVo.setOrderCode(orderCode);
storeOrderVo.setUserId(userId);
// 姓名手机号为了兼容留空或占位即可
storeOrderVo.setUserName("");
storeOrderVo.setUserMobile("");
java.math.BigDecimal skuPrice = skuInfo != null ? skuInfo.getPrice() : java.math.BigDecimal.ZERO;
java.math.BigDecimal priceTotal = skuPrice.multiply(new java.math.BigDecimal(quantity));
storeOrderVo.setPriceTotal(priceTotal);
storeOrderVo.setPriceActual(priceTotal); // 收钱吧暂无运费及优惠逻辑,实际价格等于总价
storeOrderVo.setPriceRefund(java.math.BigDecimal.ZERO);
storeOrderVo.setPriceExpress(java.math.BigDecimal.ZERO);
storeOrderVo.setPriceCoupon(java.math.BigDecimal.ZERO);
storeOrderVo.setStorePriceCoupon(java.math.BigDecimal.ZERO);
storeOrderVo.setPriceVoucher(java.math.BigDecimal.ZERO);
// 待支付
storeOrderVo.setStatus(0);
storeOrderVo.setPayType("SQB_PAY"); // 标识为收钱吧支付类型
storeOrderVo.setDeviceFrom("shouqianba");
storeOrderVo.setOrderType(0);
storeOrderVo.setWriteOffCode("EMPTY");
storeOrderVo.setPayCountdownMinute(15);
storeOrderVo.setCreatedAt(now);
// 2) 构建基础商城 SKU VO (GoblinOrderSkuVo)
String orderSkuId = IDGenerator.nextSnowId();
GoblinOrderSkuVo orderSkuVo = new GoblinOrderSkuVo();
orderSkuVo.setOrderSkuId(orderSkuId);
orderSkuVo.setOrderId(orderId);
orderSkuVo.setSpuId(spuId);
orderSkuVo.setSpuName(goodsInfo != null ? goodsInfo.getName() : "收钱吧未知商品");
orderSkuVo.setSkuId(skuId);
orderSkuVo.setNum(quantity);
orderSkuVo.setSkuPrice(skuPrice);
orderSkuVo.setSkuPriceActual(priceTotal); // 按件数总价
orderSkuVo.setSkuName(skuInfo != null ? skuInfo.getName() : "收钱吧未匹配SKU");
orderSkuVo.setSkuImage(skuInfo != null ? skuInfo.getSkuPic() : "");
orderSkuVo.setSkuSpecs("[]");
orderSkuVo.setPriceVoucher(java.math.BigDecimal.ZERO);
orderSkuVo.setCreatedAt(now);
// 特殊标识类型:33
orderSkuVo.setSkuType(33);
// 存入 Redis 并绑定关系
List<String> orderSkuVoIds = new ArrayList<>();
orderSkuVoIds.add(orderSkuId);
storeOrderVo.setOrderSkuVoIds(orderSkuVoIds);
goblinRedisUtils.setGoblinOrderSku(orderSkuId, orderSkuVo);
goblinRedisUtils.setGoblinOrder(orderId, storeOrderVo);
goblinRedisUtils.setMasterCode(masterOrderCode, orderId);
// 3) 构建收钱吧专属特有字段 (GoblinSqbOrderVo)
GoblinSqbOrderVo orderVo = new GoblinSqbOrderVo();
orderVo.setOrderId(orderId);
orderVo.setUserId(userId);
orderVo.setPerformancesId(performancesId);
orderVo.setSpuId(spuId);
orderVo.setSkuId(skuId);
orderVo.setQuantity(quantity);
orderVo.setSqbOrderSn(sqbOrderSn);
orderVo.setSqbOrderSignature(sqbOrderSignature);
orderVo.setSqbAcquiringSn(sqbAcquiringSn);
orderVo.setSqbCheckoutItemsId(checkoutItemsId);
// 内部状态同步使用
orderVo.setStatus(0);
orderVo.setCreatedAt(now);
orderVo.setUpdatedAt(now);
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
goblinRedisUtils.addOrderList(userId, orderId);
log.info("[收钱吧下单] 订单写入 Redis 成功,orderId={}", orderId);
// Step 6: 写 MongoDB + 发送复用的 SQL MQ
// 写入 Mongo
goblinMongoUtils.insertGoblinOrderSkuVo(orderSkuVo);
goblinMongoUtils.insertGoblinStoreOrderVo(storeOrderVo);
// 收钱吧扩展暂无需单独写 MongoDB 除非专门需要查询
// 拼接 SQL 数据并丢队列
java.util.LinkedList<String> sqls = new java.util.LinkedList<>();
sqls.add(com.liquidnet.service.base.SqlMapping.get("goblin.order.create.sku_insert"));
sqls.add(com.liquidnet.service.base.SqlMapping.get("goblin.order.create.order_insert"));
java.util.LinkedList<Object[]> sqlDataSku = new java.util.LinkedList<>();
sqlDataSku.add(new Object[]{
orderSkuVo.getOrderSkuId(), orderSkuVo.getOrderId(), orderSkuVo.getSpuId(), orderSkuVo.getSpuName(), (goodsInfo != null ? goodsInfo.getCoverPic() : ""),
orderSkuVo.getSkuId(), orderSkuVo.getNum(), orderSkuVo.getSkuPrice(), orderSkuVo.getSkuPriceActual(), orderSkuVo.getSkuName(),
"", orderSkuVo.getSkuImage(), orderSkuVo.getSkuSpecs(), orderSkuVo.getPriceVoucher(),
"", "", "", "", 0, // ERP 等非必要参数直接置空
orderSkuVo.getCreatedAt()
});
java.util.LinkedList<Object[]> sqlDataOrder = new java.util.LinkedList<>();
sqlDataOrder.add(new Object[]{
storeOrderVo.getMasterOrderCode(), storeOrderVo.getOrderId(), storeOrderVo.getStoreId(), storeOrderVo.getStoreName(), storeOrderVo.getOrderCode(),
storeOrderVo.getUserId(), storeOrderVo.getUserName(), storeOrderVo.getUserMobile(), storeOrderVo.getPriceTotal(), storeOrderVo.getPayCode(),
storeOrderVo.getPriceActual(), storeOrderVo.getPriceRefund(), storeOrderVo.getPriceExpress(), storeOrderVo.getPriceCoupon(), storeOrderVo.getStorePriceCoupon(),
storeOrderVo.getPriceVoucher(), storeOrderVo.getStatus(), storeOrderVo.getUcouponId(), storeOrderVo.getStoreCouponId(), storeOrderVo.getPayType(), storeOrderVo.getDeviceFrom(),
storeOrderVo.getSource(), storeOrderVo.getVersion(), storeOrderVo.getIsMember(), storeOrderVo.getOrderType(), storeOrderVo.getWriteOffCode(), storeOrderVo.getPayCountdownMinute(),
storeOrderVo.getIpAddress(), storeOrderVo.getMarketId(), storeOrderVo.getMarketType(), storeOrderVo.getCreatedAt(), "", ""
});
// 借用 Base 模块中 ConsumerGoblinOrderCPRdsReceiver 这个队列消费类进行写库
if(queueUtils != null) {
queueUtils.sendMsgByRedis(com.liquidnet.service.base.constant.MQConst.GoblinQueue.GOBLIN_ORDER_CREATE_PAY.getKey(),
com.liquidnet.service.base.SqlMapping.gets(sqls, sqlDataSku, sqlDataOrder));
// 待支付超时回调等处理也可放入原有股票队列逻辑,暂省略
}
log.info("[收钱吧下单] 完成 MongoDB 及 MQ 落库,orderId={}, masterCode={}", orderId, masterOrderCode);
// Step 7: 释放分布式锁,构建返回
goblinSqbRedisUtils.releaseOrderLock(userId, skuId);
GoblinSqbOrderCreateVo createVo = new GoblinSqbOrderCreateVo();
createVo.setOrderId(orderId);
createVo.setAcquiringSn(sqbAcquiringSn);
createVo.setTimeStamp(pv.getTimeStamp());
createVo.setPackageStr(pv.getPackageStr());
createVo.setPaySign(pv.getPaySign());
createVo.setAppId(pv.getAppId());
createVo.setSignType(pv.getSignType());
createVo.setNonceStr(pv.getNonceStr());
log.info("[收钱吧下单] 下单成功,orderId={}", orderId);
return ResponseDto.success(createVo);
} catch (Exception e) {
log.error("[收钱吧下单] 下单异常,userId={}, skuId={}", userId, skuId, e);
goblinRedisUtils.incrSkuStock(null, skuId, quantity);
goblinSqbRedisUtils.releaseOrderLock(userId, skuId);
return ResponseDto.failure("下单失败:" + e.getMessage());
}
}
// ================================ 查询支付状态 ================================
@Override
public ResponseDto<Integer> queryPayStatus(String userId, String orderId) {
log.info("[收钱吧支付状态] 开始查询 userId={}, orderId={}", userId, orderId);
GoblinSqbOrderVo orderVo = goblinSqbRedisUtils.getSqbOrder(orderId);
if (orderVo == null) {
return ResponseDto.failure("订单不存在");
}
if (!userId.equals(orderVo.getUserId())) {
return ResponseDto.failure("无权限访问该订单");
}
// 已支付直接返回
if (Integer.valueOf(1).equals(orderVo.getStatus())) {
return ResponseDto.success(1);
}
// 调用收钱吧查询收单状态
CommonRequest.Seller seller = buildSeller();
CashierQueryRequest cashierReq = buildCashierQueryRequest(seller, orderVo.getSqbAcquiringSn(), orderVo.getSqbOrderSignature());
CashierQueryData cashierData = goblinShouQianBaService.queryCashier(cashierReq);
if (cashierData == null) {
log.warn("[收钱吧支付状态] queryCashier 调用失败,orderId={}", orderId);
return ResponseDto.success(orderVo.getStatus());
}
// acquiringState 通过 cashierData.getAmount() > 0 && 收单签名有效来推断 —— 实际以收钱吧文档为准
// 简化处理:收银台能正常返回且 selectedSignature 非空,视为已支付
if (cashierData.getSelectedSignature() != null) {
String now = LocalDateTime.now().format(DTF);
orderVo.setStatus(1);
orderVo.setUpdatedAt(now);
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
syncOrderStatus(orderId, 1);
log.info("[收钱吧支付状态] 确认支付成功,orderId={}", orderId);
return ResponseDto.success(1);
}
return ResponseDto.success(orderVo.getStatus());
}
// ================================ 获取券码 ================================
@Override
public ResponseDto<GoblinSqbCouponVo> queryCoupon(String userId, String orderId) {
log.info("[收钱吧券码] 开始获取 userId={}, orderId={}", userId, orderId);
GoblinSqbOrderVo orderVo = goblinSqbRedisUtils.getSqbOrder(orderId);
if (orderVo == null) return ResponseDto.failure("订单不存在");
if (!userId.equals(orderVo.getUserId())) return ResponseDto.failure("无权限访问该订单");
if (!Integer.valueOf(1).equals(orderVo.getStatus())) return ResponseDto.failure("订单未支付,无法获取券码");
// 幂等:已存在则直接返回
if (orderVo.getCouponQrCode() != null && !orderVo.getCouponQrCode().isEmpty()) {
GoblinSqbCouponVo couponVo = new GoblinSqbCouponVo();
couponVo.setCouponSn(orderVo.getCouponSn());
couponVo.setCouponQrCode(orderVo.getCouponQrCode());
couponVo.setCouponExpireTime(orderVo.getCouponExpireTime());
return ResponseDto.success(couponVo);
}
// 调用收钱吧查询券码
CouponQueryRequest req = new CouponQueryRequest();
req.setAppid(SQB_APPID);
req.setSeller(buildSeller());
CouponQueryRequest.OrderInfo orderInfo = new CouponQueryRequest.OrderInfo();
orderInfo.setSn(orderVo.getSqbOrderSn());
orderInfo.setSignature(orderVo.getSqbOrderSignature());
req.setOrderID(orderInfo);
CouponQueryData couponData = goblinShouQianBaService.queryCoupon(req);
if (couponData == null) return ResponseDto.failure("获取券码失败");
// 更新订单 coupon 字段
String now = LocalDateTime.now().format(DTF);
orderVo.setCouponSn(couponData.getVoucherNo());
orderVo.setCouponQrCode(couponData.getUrl());
orderVo.setCouponExpireTime(couponData.getOperationTime());
orderVo.setUpdatedAt(now);
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
// 券码信息保存在 Redis,MySQL和MongoDB商城基础模型无此字段,无需同步
GoblinSqbCouponVo couponVo = new GoblinSqbCouponVo();
couponVo.setCouponSn(couponData.getVoucherNo());
couponVo.setCouponQrCode(couponData.getUrl());
couponVo.setCouponExpireTime(couponData.getOperationTime());
log.info("[收钱吧券码] 获取成功,orderId={}", orderId);
return ResponseDto.success(couponVo);
}
// ================================ 申请退款 ================================
@Override
public ResponseDto<Boolean> refund(String userId, String orderId) {
log.info("[收钱吧退款] 开始 userId={}, orderId={}", userId, orderId);
GoblinSqbOrderVo orderVo = goblinSqbRedisUtils.getSqbOrder(orderId);
if (orderVo == null) return ResponseDto.failure("订单不存在");
if (!userId.equals(orderVo.getUserId())) return ResponseDto.failure("无权限访问该订单");
if (!Integer.valueOf(1).equals(orderVo.getStatus())) return ResponseDto.failure("订单状态不可退款");
if (Integer.valueOf(1).equals(orderVo.getCouponUsedStatus())) return ResponseDto.failure("券码已核销,不可退款");
// 更新状态为退款中
String now = LocalDateTime.now().format(DTF);
orderVo.setStatus(4);
orderVo.setUpdatedAt(now);
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
syncOrderStatus(orderId, 4);
try {
// 调用收钱吧退款
CouponRefundRequest refundReq = new CouponRefundRequest();
refundReq.setAppid(SQB_APPID);
refundReq.setSeller(buildSeller());
refundReq.setRequestSource("EXTERN");
refundReq.setRequestId(IDGenerator.nextSnowId());
CouponRefundRequest.OrderInfo orderInfo = new CouponRefundRequest.OrderInfo();
orderInfo.setSn(orderVo.getSqbOrderSn());
orderInfo.setSignature(orderVo.getSqbOrderSignature());
refundReq.setOrderID(orderInfo);
CouponRefundRequest.RefundInfo refundInfo = new CouponRefundRequest.RefundInfo();
// amount 字段待接入实际金额(GoblinSqbOrderVo 需补充 amount)
refundInfo.setApplyAmount(0L); // TODO: 替换为实际订单金额
refundInfo.setType((byte) 2); // 2-按金额退款
refundInfo.setRefundReason("用户申请退款");
refundReq.setRefundInfo(refundInfo);
CouponRefundData refundData = goblinShouQianBaService.refundCoupon(refundReq);
if (refundData == null) {
// 退款失败,回滚状态
orderVo.setStatus(1);
orderVo.setUpdatedAt(LocalDateTime.now().format(DTF));
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
syncOrderStatus(orderId, 1);
return ResponseDto.failure("退款失败,请稍后重试");
}
// 退款成功
orderVo.setStatus(3);
orderVo.setUpdatedAt(LocalDateTime.now().format(DTF));
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
goblinRedisUtils.incrSkuStock(null, orderVo.getSkuId(), orderVo.getQuantity());
syncOrderStatus(orderId, 3);
log.info("[收钱吧退款] 退款成功,orderId={}", orderId);
return ResponseDto.success(Boolean.TRUE);
} catch (Exception e) {
log.error("[收钱吧退款] 退款异常,orderId={},回滚 status=1", orderId, e);
orderVo.setStatus(1);
orderVo.setUpdatedAt(LocalDateTime.now().format(DTF));
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
syncOrderStatus(orderId, 1);
return ResponseDto.failure("退款失败:" + e.getMessage());
}
}
// ================================ 同步核销状态 ================================
@Override
public ResponseDto<Boolean> syncCouponStatus(String userId, String orderId) {
log.info("[收钱吧核销同步] 开始 userId={}, orderId={}", userId, orderId);
GoblinSqbOrderVo orderVo = goblinSqbRedisUtils.getSqbOrder(orderId);
if (orderVo == null) return ResponseDto.failure("订单不存在");
if (!userId.equals(orderVo.getUserId())) return ResponseDto.failure("无权限访问该订单");
CouponStatusSyncRequest req = new CouponStatusSyncRequest();
req.setAppid(SQB_APPID);
req.setRedeemSource("EXTERN");
// TODO: 填充 voucherNos(券号列表)、redeemMerchantId、redeemExternalOrderSn、clientSn、status
// 需要从 orderVo 中获取 couponSn 和其他必要字段
if (orderVo.getCouponSn() != null) {
req.setVoucherNos(java.util.Collections.singletonList(orderVo.getCouponSn()));
}
req.setRedeemMerchantId(SQB_MERCHANT_ID);
req.setRedeemExternalOrderSn(IDGenerator.nextSnowId());
req.setClientSn("terminal_001"); // TODO: 配置终端号
req.setStatus((byte) 1); // 1-已核销
boolean couponUsed = goblinShouQianBaService.syncCouponStatus(req);
if (couponUsed) {
String now = LocalDateTime.now().format(DTF);
orderVo.setStatus(2);
orderVo.setCouponUsedStatus(1);
orderVo.setUpdatedAt(now);
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
syncOrderStatus(orderId, 2);
log.info("[收钱吧核销同步] 核销成功,orderId={}", orderId);
}
return ResponseDto.success(couponUsed);
}
// ================================ 回调处理 ================================
@Override
public ResponseDto<String> handlePayCallback(Map<String, Object> params) {
log.info("[收钱吧支付回调] 收到回调参数: {}", params);
String orderId = getOrderIdFromParams(params);
if (orderId == null) return ResponseDto.failure("缺少订单标识");
GoblinSqbOrderVo orderVo = goblinSqbRedisUtils.getSqbOrder(orderId);
if (orderVo == null) return ResponseDto.failure("订单不存在");
if (Integer.valueOf(1).equals(orderVo.getStatus())) return ResponseDto.success("success"); // 幂等
String now = LocalDateTime.now().format(DTF);
orderVo.setStatus(1);
orderVo.setUpdatedAt(now);
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
syncOrderStatus(orderId, 1);
log.info("[收钱吧支付回调] 更新支付状态成功,orderId={}", orderId);
return ResponseDto.success("success");
}
@Override
public ResponseDto<String> handleRefundCallback(Map<String, Object> params) {
log.info("[收钱吧退款回调] 收到回调参数: {}", params);
String orderId = getOrderIdFromParams(params);
if (orderId == null) return ResponseDto.failure("缺少订单标识");
GoblinSqbOrderVo orderVo = goblinSqbRedisUtils.getSqbOrder(orderId);
if (orderVo == null) return ResponseDto.failure("订单不存在");
if (Integer.valueOf(3).equals(orderVo.getStatus())) return ResponseDto.success("success"); // 幂等
String now = LocalDateTime.now().format(DTF);
orderVo.setStatus(3);
orderVo.setUpdatedAt(now);
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
goblinRedisUtils.incrSkuStock(null, orderVo.getSkuId(), orderVo.getQuantity());
syncOrderStatus(orderId, 3);
log.info("[收钱吧退款回调] 更新退款状态成功,orderId={}", orderId);
return ResponseDto.success("success");
}
@Override
public ResponseDto<String> handleCouponCallback(Map<String, Object> params) {
log.info("[收钱吧券状态回调] 收到回调参数: {}", params);
String orderId = getOrderIdFromParams(params);
if (orderId == null) return ResponseDto.failure("缺少订单标识");
GoblinSqbOrderVo orderVo = goblinSqbRedisUtils.getSqbOrder(orderId);
if (orderVo == null) return ResponseDto.failure("订单不存在");
if (Integer.valueOf(2).equals(orderVo.getStatus())) return ResponseDto.success("success"); // 幂等
String now = LocalDateTime.now().format(DTF);
orderVo.setStatus(2);
orderVo.setCouponUsedStatus(1);
orderVo.setUpdatedAt(now);
goblinSqbRedisUtils.setSqbOrder(orderId, orderVo);
syncOrderStatus(orderId, 2);
log.info("[收钱吧券状态回调] 核销状态更新成功,orderId={}", orderId);
return ResponseDto.success("success");
}
// ================================ 自动退款(定时任务) ================================
@Override
public ResponseDto<String> autoRefundByPerformance(String performancesId) {
log.info("[收钱吧自动退款] 开始处理演出 performancesId={}", performancesId);
// 查询该演出下 status=1(已支付)的订单(MySQL)
// TODO: 通过 GoblinSqbOrderMapper 查询(需注入后解注释)
// LambdaQueryWrapper<GoblinSqbOrder> query = new LambdaQueryWrapper<>();
// query.eq(GoblinSqbOrder::getPerformancesId, performancesId).eq(GoblinSqbOrder::getStatus, 1);
// List<GoblinSqbOrder> orders = goblinSqbOrderMapper.selectList(query);
int successCount = 0;
int failCount = 0;
// TODO: 取消注释并接入 GoblinSqbOrderMapper 后启用
// for (GoblinSqbOrder order : orders) {
// try {
// ResponseDto<Boolean> result = refund(order.getUserId(), order.getOrderId());
// if (result != null && result.isSuccess()) {
// successCount++;
// } else {
// log.warn("[收钱吧自动退款] 订单退款失败 orderId={}", order.getOrderId());
// failCount++;
// }
// } catch (Exception e) {
// log.error("[收钱吧自动退款] 订单退款异常 orderId={}", order.getOrderId(), e);
// failCount++;
// }
// }
String summary = String.format("演出 %s 自动退款完成:成功 %d 笔,失败 %d 笔", performancesId, successCount, failCount);
log.info("[收钱吧自动退款] {}", summary);
return ResponseDto.success(summary);
}
// ================================ 订单列表 ================================
@Override
public ResponseDto<List<GoblinSqbOrderDetailVo>> getOrderList(String userId) {
log.info("[收钱吧订单列表] userId={}", userId);
List<String> orderIds = goblinRedisUtils.getOrderList(userId);
if (CollectionUtils.isEmpty(orderIds)) {
return ResponseDto.success(Collections.emptyList());
}
List<GoblinSqbOrderDetailVo> result = new ArrayList<>();
for (String orderId : orderIds) {
GoblinStoreOrderVo storeOrderVo = goblinRedisUtils.getGoblinOrder(orderId);
if (storeOrderVo == null) continue;
// 取第一个 orderSkuId 判断 skuType
List<String> orderSkuVoIds = storeOrderVo.getOrderSkuVoIds();
if (CollectionUtils.isEmpty(orderSkuVoIds)) continue;
GoblinOrderSkuVo skuVo = goblinRedisUtils.getGoblinOrderSkuVo(orderSkuVoIds.get(0));
if (skuVo == null) continue;
// 过滤:只处理 skuType=33 的收钱吧订单
if (!Integer.valueOf(33).equals(skuVo.getSkuType())) continue;
GoblinSqbOrderVo sqbOrderVo = goblinSqbRedisUtils.getSqbOrder(orderId);
result.add(buildDetailVo(storeOrderVo, skuVo, sqbOrderVo));
}
// 按创建时间倒序排列
result.sort((a, b) -> {
if (a.getCreatedAt() == null) return 1;
if (b.getCreatedAt() == null) return -1;
return b.getCreatedAt().compareTo(a.getCreatedAt());
});
log.info("[收钱吧订单列表] userId={}, 共 {} 条", userId, result.size());
return ResponseDto.success(result);
}
// ================================ 订单详情 ================================
@Override
public ResponseDto<GoblinSqbOrderDetailVo> getOrderDetail(String userId, String orderId) {
log.info("[收钱吧订单详情] userId={}, orderId={}", userId, orderId);
GoblinStoreOrderVo storeOrderVo = goblinRedisUtils.getGoblinOrder(orderId);
if (storeOrderVo == null) return ResponseDto.failure("订单不存在");
if (!userId.equals(storeOrderVo.getUserId())) return ResponseDto.failure("无权限访问该订单");
List<String> orderSkuVoIds = storeOrderVo.getOrderSkuVoIds();
if (CollectionUtils.isEmpty(orderSkuVoIds)) return ResponseDto.failure("订单数据异常");
GoblinOrderSkuVo skuVo = goblinRedisUtils.getGoblinOrderSkuVo(orderSkuVoIds.get(0));
if (skuVo == null) return ResponseDto.failure("订单商品数据异常");
// 校验是收钱吧订单
if (!Integer.valueOf(33).equals(skuVo.getSkuType())) {
return ResponseDto.failure("该订单非收钱吧订单");
}
GoblinSqbOrderVo sqbOrderVo = goblinSqbRedisUtils.getSqbOrder(orderId);
GoblinSqbOrderDetailVo detailVo = buildDetailVo(storeOrderVo, skuVo, sqbOrderVo);
log.info("[收钱吧订单详情] 查询成功,orderId={}", orderId);
return ResponseDto.success(detailVo);
}
// ================================ 再次付款 ================================
@Override
public ResponseDto<GoblinSqbOrderCreateVo> repay(String userId, String orderId) {
log.info("[收钱吧再次付款] userId={}, orderId={}", userId, orderId);
GoblinSqbOrderVo orderVo = goblinSqbRedisUtils.getSqbOrder(orderId);
if (orderVo == null) return ResponseDto.failure("订单不存在");
if (!userId.equals(orderVo.getUserId())) return ResponseDto.failure("无权限访问该订单");
if (!Integer.valueOf(0).equals(orderVo.getStatus())) {
return ResponseDto.failure("订单状态不支持再次付款(仅待支付状态可重新拉起)");
}
if (orderVo.getSqbAcquiringSn() == null) {
return ResponseDto.failure("订单数据异常,缺少收单号");
}
try {
CommonRequest.Seller seller = buildSeller();
// 重新查询收银台,获取新的 selectedSignature + seq
CashierQueryRequest cashierReq = buildCashierQueryRequest(
seller, orderVo.getSqbAcquiringSn(), orderVo.getSqbOrderSignature());
CashierQueryData cashierData = goblinShouQianBaService.queryCashier(cashierReq);
if (cashierData == null) return ResponseDto.failure("查询收银台失败,请稍后重试");
// 重新创建微信预支付
CreateWechatPrepayOrderRequest prepayReq = buildWechatPrepayRequest(
seller, orderVo.getSqbAcquiringSn(), orderVo.getSqbOrderSignature(),
cashierData.getSelectedSignature(), cashierData.getSeq(), cashierData);
CreateWechatPrepayOrderData prepayData = goblinShouQianBaService.createWechatPrepayOrder(prepayReq);
if (prepayData == null || prepayData.getPaymentVoucher() == null) {
return ResponseDto.failure("创建微信预支付失败,请稍后重试");
}
CreateWechatPrepayOrderData.PaymentVoucher pv = prepayData.getPaymentVoucher();
GoblinSqbOrderCreateVo createVo = new GoblinSqbOrderCreateVo();
createVo.setOrderId(orderId);
createVo.setAcquiringSn(orderVo.getSqbAcquiringSn());
createVo.setTimeStamp(pv.getTimeStamp());
createVo.setPackageStr(pv.getPackageStr());
createVo.setPaySign(pv.getPaySign());
createVo.setAppId(pv.getAppId());
createVo.setSignType(pv.getSignType());
createVo.setNonceStr(pv.getNonceStr());
log.info("[收钱吧再次付款] 成功,orderId={}", orderId);
return ResponseDto.success(createVo);
} catch (Exception e) {
log.error("[收钱吧再次付款] 异常,orderId={}", orderId, e);
return ResponseDto.failure("再次付款失败:" + e.getMessage());
}
}
// ================================ 私有辅助方法 ================================
private CommonRequest.Seller buildSeller() {
CommonRequest.Seller seller = new CommonRequest.Seller();
seller.setMerchantId(SQB_MERCHANT_ID);
seller.setMerchantUserId(SQB_MERCHANT_USER_ID);
seller.setRole(SQB_ROLE);
return seller;
}
private CashierQueryRequest buildCashierQueryRequest(CommonRequest.Seller seller,
String acquiringSn, String acquiringSignature) {
CashierQueryRequest req = new CashierQueryRequest();
req.setAppid(SQB_APPID);
req.setSeller(seller);
req.setPaymentMode(4); // 4-微信小程序支付
CashierQueryRequest.PaymentEnv paymentEnv = new CashierQueryRequest.PaymentEnv();
paymentEnv.setClient("wechat");
req.setPaymentEnv(paymentEnv);
CommonRequest.Acquiring acquiring = new CommonRequest.Acquiring();
acquiring.setAcquiringSn(acquiringSn);
acquiring.setSignature(acquiringSignature);
req.setAcquiringInfo(acquiring);
return req;
}
private CreateWechatPrepayOrderRequest buildWechatPrepayRequest(CommonRequest.Seller seller,
String acquiringSn,
String acquiringSignature,
String selectedSignature,
String seq,
CashierQueryData cashierData) {
CreateWechatPrepayOrderRequest req = new CreateWechatPrepayOrderRequest();
req.setAppid(SQB_APPID);
req.setSeller(seller);
req.setAcquiringSn(acquiringSn);
req.setSignature(acquiringSignature);
req.setSelectedSignature(selectedSignature);
req.setSeq(seq);
// 使用收银台返回的第一个可用支付工具
if (cashierData != null && !CollectionUtils.isEmpty(cashierData.getPayTools())) {
List<CreateWechatPrepayOrderRequest.UsingPayTool> usingPayTools = new ArrayList<>();
cashierData.getPayTools().stream()
.filter(t -> Boolean.TRUE.equals(t.getSelectable()))
.findFirst()
.ifPresent(payTool -> {
CreateWechatPrepayOrderRequest.UsingPayTool usingTool = new CreateWechatPrepayOrderRequest.UsingPayTool();
usingTool.setId(payTool.getId());
usingTool.setPayTool(payTool.getType());
usingTool.setPayMode(4);
usingTool.setAmount(payTool.getShowAmount() != null ? String.valueOf(payTool.getShowAmount()) : "0");
usingTool.setIdentity("wzwl");
usingTool.setRequestSn(IDGenerator.nextSnowId());
usingPayTools.add(usingTool);
});
req.setUsingPayTools(usingPayTools);
}
return req;
}
private GoblinSqbOrderDetailVo buildDetailVo(GoblinStoreOrderVo storeOrderVo,
GoblinOrderSkuVo skuVo,
GoblinSqbOrderVo sqbOrderVo) {
GoblinSqbOrderDetailVo detailVo = new GoblinSqbOrderDetailVo();
// 来自 GoblinStoreOrderVo
detailVo.setOrderId(storeOrderVo.getOrderId());
detailVo.setOrderCode(storeOrderVo.getOrderCode());
detailVo.setStatus(storeOrderVo.getStatus());
detailVo.setPriceActual(storeOrderVo.getPriceActual());
detailVo.setCreatedAt(storeOrderVo.getCreatedAt());
detailVo.setPayTime(storeOrderVo.getPayTime());
// 来自 GoblinOrderSkuVo
detailVo.setSpuId(skuVo.getSpuId());
detailVo.setSpuName(skuVo.getSpuName());
detailVo.setSkuId(skuVo.getSkuId());
detailVo.setSkuName(skuVo.getSkuName());
detailVo.setSkuImage(skuVo.getSkuImage());
detailVo.setQuantity(skuVo.getNum());
// 来自 GoblinSqbOrderVo
if (sqbOrderVo != null) {
detailVo.setSqbStatus(sqbOrderVo.getStatus());
detailVo.setPerformancesId(sqbOrderVo.getPerformancesId());
detailVo.setSqbAcquiringSn(sqbOrderVo.getSqbAcquiringSn());
detailVo.setCouponSn(sqbOrderVo.getCouponSn());
detailVo.setCouponQrCode(sqbOrderVo.getCouponQrCode());
detailVo.setCouponExpireTime(sqbOrderVo.getCouponExpireTime());
detailVo.setCouponUsedStatus(sqbOrderVo.getCouponUsedStatus());
}
return detailVo;
}
private String getOrderIdFromParams(Map<String, Object> params) {
String orderId = (String) params.get("orderId");
if (orderId == null) orderId = (String) params.get("acquiringSn");
return orderId;
}
private void syncOrderStatus(String orderId, int status) {
GoblinStoreOrderVo storeOrderVo = goblinRedisUtils.getGoblinOrder(orderId);
if (storeOrderVo == null) return;
String now = LocalDateTime.now().format(DTF);
storeOrderVo.setStatus(status);
if (status == 1) { // 支付成功
storeOrderVo.setPayTime(now);
}
// 1. 同步 Redis
goblinRedisUtils.setGoblinOrder(orderId, storeOrderVo);
// 2. 同步 MongoDB
goblinMongoUtils.updateGoblinStoreOrderVo(orderId, storeOrderVo);
// 3. 同步 MySQL (复用下单 MQ 发送 Update 语句)
String sql;
Object[] data;
if (status == 1) {
sql = "UPDATE goblin_store_order SET status = ?, pay_time = ?, updated_at = ? WHERE order_id = ?";
data = new Object[]{status, now, now, orderId};
} else {
sql = "UPDATE goblin_store_order SET status = ?, updated_at = ? WHERE order_id = ?";
data = new Object[]{status, now, orderId};
}
java.util.LinkedList<String> sqls = new java.util.LinkedList<>();
sqls.add(sql);
java.util.LinkedList<Object[]> sqlData = new java.util.LinkedList<>();
sqlData.add(data);
if (queueUtils != null) {
queueUtils.sendMsgByRedis(com.liquidnet.service.base.constant.MQConst.GoblinQueue.GOBLIN_ORDER_CREATE_PAY.getKey(),
com.liquidnet.service.base.SqlMapping.gets(sqls, sqlData));
}
}
}
......@@ -1697,6 +1697,11 @@ public class GoblinRedisUtils {
redisUtil.set(redisKey, list);
}
//主订单下包含的子订单
public void setMasterCode(String masterCode, String orderIds) {
String redisKey = GoblinRedisConst.REDIS_GOBLIN_ORDER_MASTER.concat(masterCode);
redisUtil.set(redisKey, orderIds);
}
public String[] getMasterCode(String masterCode) {
String redisKey = GoblinRedisConst.REDIS_GOBLIN_ORDER_MASTER.concat(masterCode);
......
package com.liquidnet.service.goblin.util;
import com.fasterxml.jackson.core.type.TypeReference;
import com.liquidnet.common.cache.redis.util.RedisUtil;
import com.liquidnet.commons.lang.util.JsonUtils;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbOrderVo;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbPerfGoodsVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 收钱吧相关 Redis 操作封装
*/
@Slf4j
@Component
public class GoblinSqbRedisUtils {
// Redis Key 前缀
private static final String KEY_SQB_ORDER = "goblin:sqb:order:";
private static final String KEY_SQB_PERF_GOODS = "goblin:sqb:perf:goods:";
private static final String KEY_SQB_ORDER_LOCK = "goblin:sqb:order:lock:";
// TTL 常量(秒)
private static final long TTL_ORDER = 2 * 60 * 60; // 2h
private static final long TTL_PERF_GOODS = 5 * 60; // 5min
private static final long TTL_ORDER_LOCK = 10; // 10s
@Autowired
private RedisUtil redisUtil;
/* ---------------------------------------- 订单操作(TTL 2h) ---------------------------------------- */
public void setSqbOrder(String orderId, GoblinSqbOrderVo vo) {
redisUtil.set(KEY_SQB_ORDER.concat(orderId), JsonUtils.toJson(vo), TTL_ORDER);
}
public GoblinSqbOrderVo getSqbOrder(String orderId) {
String valStr = (String) redisUtil.get(KEY_SQB_ORDER.concat(orderId));
if (StringUtils.isEmpty(valStr)) {
return null;
}
return JsonUtils.fromJson(valStr, GoblinSqbOrderVo.class);
}
public void delSqbOrder(String orderId) {
redisUtil.del(KEY_SQB_ORDER.concat(orderId));
}
/* ---------------------------------------- 演出关联商品缓存(TTL 5min) ---------------------------------------- */
public void setPerfGoods(String performancesId, List<GoblinSqbPerfGoodsVo> list) {
redisUtil.set(KEY_SQB_PERF_GOODS.concat(performancesId), JsonUtils.toJson(list), TTL_PERF_GOODS);
}
public List<GoblinSqbPerfGoodsVo> getPerfGoods(String performancesId) {
String valStr = (String) redisUtil.get(KEY_SQB_PERF_GOODS.concat(performancesId));
if (StringUtils.isEmpty(valStr)) {
return null;
}
return JsonUtils.fromJson(valStr, new TypeReference<List<GoblinSqbPerfGoodsVo>>() {});
}
public void delPerfGoods(String performancesId) {
redisUtil.del(KEY_SQB_PERF_GOODS.concat(performancesId));
}
/* ---------------------------------------- 下单防重锁(TTL 10s) ---------------------------------------- */
/**
* 尝试获取下单防重锁
*
* @param userId 用户ID
* @param skuId SKU ID
* @return true 获取成功,false 已被锁定
*/
public boolean tryOrderLock(String userId, String skuId) {
String key = KEY_SQB_ORDER_LOCK.concat(userId).concat(":").concat(skuId);
return redisUtil.lock(key, 1, TTL_ORDER_LOCK);
}
/**
* 释放下单防重锁
*
* @param userId 用户ID
* @param skuId SKU ID
*/
public void releaseOrderLock(String userId, String skuId) {
String key = KEY_SQB_ORDER_LOCK.concat(userId).concat(":").concat(skuId);
redisUtil.uLock(key);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment