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

Commit 583f5927 authored by 姜秀龙's avatar 姜秀龙

收钱吧 admin商品绑定和 redis 相关优化

parent d7836fd9
......@@ -425,9 +425,18 @@ public class GoblinRedisConst {
/* ----------------------------------------------------------------- */
public static final String ERP_GOBLIN_GOODS_LIST = PREFIX.concat("erp:push:order:");
/* ----------------------------------------------------------------- */
/* ----------------------------------------------------------------- */
/* ----------------------------------------------------------------- */
/* ----------------------------------------------------------------- */
/* --------------------------------收钱吧相关--------------------------------- */
/**
* 收钱吧订单详情
*/
public static final String SQB_ORDER = PREFIX.concat("sqb:order:");
/**
* 演出关联收钱吧商品缓存
*/
public static final String SQB_PERFORMANCE_GOODS = PREFIX.concat("sqb:perf:goods:");
/**
* 收钱吧下单防重锁
*/
public static final String SQB_ORDER_LOCK = PREFIX.concat("sqb:order:lock:");
}
......@@ -2,6 +2,7 @@ package com.liquidnet.service.goblin.dto.vo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
public class GoblinSqbPerfGoodsVo implements Serializable {
......@@ -10,7 +11,16 @@ public class GoblinSqbPerfGoodsVo implements Serializable {
private String spuName;
private String skuId;
private String skuName;
/** 商品原价(分/元,与原有 price 字段保持向后兼容) */
private Long price;
private String coverPic;
private Integer sort;
/** 换购价格(为 null 时按售价) */
private BigDecimal settlementPrice;
/** 演出结束自动下架 0-否 1-是 */
private Integer autoOffline;
/** 商品库存(管理后台展示用) */
private Integer stock;
/** 商品状态(管理后台展示用) */
private Integer status;
}
package com.liquidnet.client.admin.web.controller.zhengzai.goblin;
import com.liquidnet.client.admin.common.core.controller.BaseController;
import com.liquidnet.client.admin.common.core.domain.AjaxResult;
import com.liquidnet.client.admin.zhengzai.goblin.dto.SqbPerfGoodsBindItemParam;
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.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 演出-收钱吧商品关联管理接口
* 演出-收钱吧商品关联管理
*/
@Slf4j
@RestController
@Controller
@Api(tags = "收钱吧-演出商品关联管理")
@RequestMapping("sqb/performance/goods")
public class SqbPerformanceGoodsController extends BaseController {
private final String prefix = "zhengzai/goblin/sqbGoods";
@Autowired
private ISqbPerformanceGoodsService sqbPerformanceGoodsService;
/**
* 关联商品管理页面(iframe 内嵌)
*/
@GetMapping("page/{performancesId}")
public String sqbGoodsPage(@PathVariable("performancesId") String performancesId, ModelMap mmap) {
mmap.put("performancesId", performancesId);
return prefix + "/index";
}
/**
* 搜索可选商品(skuType=33 的收钱吧商品)
*/
@GetMapping("search")
@ResponseBody
@ApiOperation("搜索收钱吧候选商品")
@ApiImplicitParams({
@ApiImplicitParam(type = "query", dataType = "String", name = "keyword", value = "关键词", required = false),
})
public AjaxResult searchGoods(@RequestParam(value = "keyword", required = false) String keyword) {
ResponseDto<List<GoblinSqbPerfGoodsVo>> resp = sqbPerformanceGoodsService.searchGoods(keyword);
if (resp.isSuccess()) {
return AjaxResult.success(resp.getData());
}
return AjaxResult.error(resp.getMessage());
}
/**
* 批量绑定/保存关联商品配置
*/
@PostMapping("bind")
@ApiOperation("关联演出与商品")
@ApiResponse(code = 200, message = "接口返回对象参数")
@ResponseBody
@ApiOperation("批量绑定演出与商品(含换购价、自动下架配置)")
@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);
public AjaxResult bind(@RequestParam("performancesId") String performancesId,
@RequestBody List<SqbPerfGoodsBindItemParam> items) {
ResponseDto<Boolean> resp = sqbPerformanceGoodsService.bind(performancesId, items);
if (resp.isSuccess()) {
return AjaxResult.success("保存成功");
}
return AjaxResult.error(resp.getMessage());
}
@DeleteMapping("unbind")
/**
* 解除关联
*/
@PostMapping("unbind")
@ResponseBody
@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,
public AjaxResult unbind(@RequestParam("performancesId") String performancesId,
@RequestParam("skuId") String skuId) {
return sqbPerformanceGoodsService.unbind(performancesId, skuId);
ResponseDto<Boolean> resp = sqbPerformanceGoodsService.unbind(performancesId, skuId);
if (resp.isSuccess()) {
return AjaxResult.success("已取消关联");
}
return AjaxResult.error(resp.getMessage());
}
/**
* 查询演出已关联商品列表
*/
@GetMapping("list")
@ResponseBody
@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);
public AjaxResult list(@RequestParam("performancesId") String performancesId) {
ResponseDto<List<GoblinSqbPerfGoodsVo>> resp = sqbPerformanceGoodsService.list(performancesId);
if (resp.isSuccess()) {
return AjaxResult.success(resp.getData());
}
return AjaxResult.error(resp.getMessage());
}
}
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('关联推荐商品')"/>
<link rel="stylesheet" th:href="@{/ajax/libs/select2/select2.min.css}"/>
<style>
body { background: #fff; }
.page-wrap { padding: 16px; }
.tip { color: #e8501a; font-size: 12px; margin-left: 8px; }
.section-title { font-weight: bold; margin: 16px 0 8px; }
.auto-offline-bar { margin-bottom: 10px; font-size: 13px; }
.settlement-input { width: 100px; }
.img-thumb { width: 40px; height: 40px; object-fit: cover; border-radius: 4px; }
.select2-container { min-width: 380px; }
.add-row { margin-bottom: 16px; display: flex; align-items: center; gap: 10px; }
</style>
</head>
<body class="white-bg">
<div class="page-wrap">
<div class="section-title">
关联推荐商品
<span class="tip">如未关联商品,则APP和小程序前端推荐模块隐藏</span>
</div>
<!-- Select2 选择框 -->
<div class="add-row">
<select id="goodsSelect" style="width:400px;">
<option value="">请输入商品名称搜索...</option>
</select>
<span style="color:#999;font-size:12px;">选中商品后自动添加到列表</span>
</div>
<!-- 已关联商品 -->
<div class="section-title">
已关联商品
<span class="tip">⚠ 设置换购价格,则商品参与演出票换购活动,不设置则按售价售卖,换购价须小于商品售价</span>
</div>
<div class="auto-offline-bar">
<label>
<input type="checkbox" id="globalAutoOffline" onchange="toggleGlobalAutoOffline(this)"/>
演出结束商品自动下架
</label>
</div>
<div class="table-responsive">
<table class="table table-bordered" id="linkedGoodsTable">
<thead>
<tr>
<th>商品ID</th>
<th>商品头图</th>
<th>商品名称</th>
<th>商品售价(元)</th>
<th>换购价(元)</th>
<th>商品库存</th>
<th>自动下架</th>
<th>操作</th>
</tr>
</thead>
<tbody id="linkedGoodsBody">
<tr id="emptyRow">
<td colspan="8" style="text-align:center;color:#999;">暂无关联商品</td>
</tr>
</tbody>
</table>
</div>
<button type="button" class="btn btn-primary" onclick="saveConfig()">保存配置</button>
</div>
<th:block th:include="include :: footer"/>
<script th:src="@{/ajax/libs/select2/select2.min.js}"></script>
<script th:inline="javascript">
var performancesId = /*[[${performancesId}]]*/ '';
var prefix = ctx + 'sqb/performance/goods';
// 内存中维护已关联商品列表
var linkedGoods = [];
// ——— mock 数据(接口无数据时用于测试)———
var MOCK_GOODS = [
{ skuId: 'SKU_MOCK_001', spuId: 'SPU_MOCK_001', spuName: '收钱吧周边T恤', skuName: '收钱吧周边T恤 M码', price: 9900, stock: 100, coverPic: '' },
{ skuId: 'SKU_MOCK_002', spuId: 'SPU_MOCK_002', spuName: '演出纪念手册', skuName: '演出纪念手册 精装版',price: 4900, stock: 200, coverPic: '' },
{ skuId: 'SKU_MOCK_003', spuId: 'SPU_MOCK_003', spuName: '限定徽章套装', skuName: '限定徽章套装 五件套', price: 3900, stock: 50, coverPic: '' },
{ skuId: 'SKU_MOCK_004', spuId: 'SPU_MOCK_004', spuName: '摇滚帽子', skuName: '摇滚帽子 黑色', price: 5900, stock: 80, coverPic: '' },
];
$(function () {
initSelect2();
loadLinkedGoods();
});
// 初始化 Select2(AJAX 搜索 + mock fallback)
function initSelect2() {
$('#goodsSelect').select2({
allowClear: true,
placeholder: '请输入商品名称搜索...',
minimumInputLength: 0,
language: { inputTooShort: function () { return '请输入关键词'; } },
ajax: {
url: prefix + '/search',
dataType: 'json',
delay: 300,
data: function (params) { return { keyword: params.term || '' }; },
processResults: function (resp) {
var list = (resp && resp.data) ? resp.data : [];
// 接口无数据时回退到 mock
if (list.length === 0) { list = filterMock($('#goodsSelect').data('select2').options.get('ajax').data({ term: '' }).keyword); }
return { results: list.map(function (item) {
return { id: item.skuId, text: (item.spuName || item.skuName), _raw: item };
})};
},
error: function () {
// 接口异常时使用 mock
return { results: MOCK_GOODS.map(function (item) {
return { id: item.skuId, text: item.spuName, _raw: item };
})};
}
}
});
// 选中即添加
$('#goodsSelect').on('select2:select', function (e) {
var raw = e.params.data._raw;
if (!raw) {
// 兼容:从 mock 中找
raw = MOCK_GOODS.find(function(m){ return m.skuId === e.params.data.id; });
}
if (raw) { addLinked(raw); }
// 清空选择框
$(this).val(null).trigger('change');
});
// 若 ajax 报错或首次打开无关键词,预填 mock
$('#goodsSelect').on('select2:open', function () {
// 触发一次搜索以显示 mock 数据
setTimeout(function () {
var searchInput = document.querySelector('.select2-search__field');
if (searchInput) { $(searchInput).trigger('input'); }
}, 100);
});
}
function filterMock(keyword) {
if (!keyword) return MOCK_GOODS;
return MOCK_GOODS.filter(function (m) { return m.spuName.indexOf(keyword) >= 0; });
}
// 加载已关联商品
function loadLinkedGoods() {
$.get(prefix + '/list', { performancesId: performancesId }, function (resp) {
if (resp.code === 0 && resp.data && resp.data.length > 0) {
linkedGoods = resp.data.map(function (item) {
return {
skuId: item.skuId, spuId: item.spuId,
spuName: item.spuName || item.skuName, skuName: item.skuName,
coverPic: item.coverPic || '', price: item.price, stock: item.stock,
settlementPrice: item.settlementPrice || '', autoOffline: item.autoOffline || 0
};
});
renderLinkedGoods();
}
});
}
function renderLinkedGoods() {
var tbody = document.getElementById('linkedGoodsBody');
if (linkedGoods.length === 0) {
tbody.innerHTML = '<tr id="emptyRow"><td colspan="8" style="text-align:center;color:#999;">暂无关联商品</td></tr>';
return;
}
var html = '';
linkedGoods.forEach(function (item, idx) {
var priceYuan = item.price ? (item.price / 100).toFixed(2) : '-';
var settlementVal = item.settlementPrice || '';
var autoChecked = item.autoOffline == 1 ? 'checked' : '';
var imgHtml = item.coverPic
? '<img src="' + item.coverPic + '" class="img-thumb"/>'
: '<span style="color:#ccc;font-size:12px;">无图</span>';
html += '<tr data-idx="' + idx + '">';
html += '<td><small>' + item.skuId + '</small></td>';
html += '<td>' + imgHtml + '</td>';
html += '<td>' + (item.spuName || item.skuName || '-') + '</td>';
html += '<td>' + priceYuan + '</td>';
html += '<td><input type="number" class="form-control settlement-input" value="' + settlementVal
+ '" placeholder="不填按售价" onchange="updateSettlement(' + idx + ', this.value)" min="0" step="0.01"/></td>';
html += '<td>' + (item.stock != null ? item.stock : '-') + '</td>';
html += '<td><input type="checkbox" ' + autoChecked + ' onchange="updateAutoOffline(' + idx + ', this)"/></td>';
html += '<td><button type="button" class="btn btn-xs btn-danger" onclick="removeLinked(' + idx + ')">取消关联</button></td>';
html += '</tr>';
});
tbody.innerHTML = html;
}
function addLinked(item) {
for (var i = 0; i < linkedGoods.length; i++) {
if (linkedGoods[i].skuId === item.skuId) {
layer.msg('该商品已在关联列表中');
return;
}
}
linkedGoods.push({
skuId: item.skuId, spuId: item.spuId || '',
spuName: item.spuName, skuName: item.skuName,
coverPic: item.coverPic || '', price: item.price, stock: item.stock,
settlementPrice: null, autoOffline: 0
});
renderLinkedGoods();
layer.msg('已添加,可配置换购价后保存');
}
function updateSettlement(idx, val) {
linkedGoods[idx].settlementPrice = val ? parseFloat(val) : null;
}
function updateAutoOffline(idx, checkbox) {
linkedGoods[idx].autoOffline = checkbox.checked ? 1 : 0;
}
function toggleGlobalAutoOffline(checkbox) {
var val = checkbox.checked ? 1 : 0;
linkedGoods.forEach(function (item) { item.autoOffline = val; });
renderLinkedGoods();
}
function removeLinked(idx) {
var skuId = linkedGoods[idx].skuId;
// mock 数据直接从内存移除,无需请求接口
if (skuId.startsWith('SKU_MOCK_')) {
linkedGoods.splice(idx, 1);
renderLinkedGoods();
return;
}
if (!confirm('确定取消关联该商品?')) return;
$.post(prefix + '/unbind', { performancesId: performancesId, skuId: skuId }, function (resp) {
if (resp.code === 0) {
linkedGoods.splice(idx, 1);
renderLinkedGoods();
} else {
alert(resp.msg || resp.message || '操作失败');
}
});
}
function saveConfig() {
var items = linkedGoods.map(function (item) {
return {
skuId: item.skuId, sort: 0,
settlementPrice: item.settlementPrice || null,
autoOffline: item.autoOffline || 0
};
});
$.ajax({
url: prefix + '/bind?performancesId=' + encodeURIComponent(performancesId),
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(items),
success: function (resp) {
if (resp.code === 0) {
layer.msg('保存成功');
} else {
layer.msg(resp.msg || resp.message || '保存失败', { icon: 2 });
}
},
error: function () {
layer.msg('请求失败,请重试', { icon: 2 });
}
});
}
</script>
</body>
</html>
......@@ -45,6 +45,8 @@
</li>
<li id="li-tab-12"><a data-toggle="tab" href="#tab-12" aria-expanded="false" onclick="artistLineupInfo()">演出阵容</a>
</li>
<li id="li-tab-13"><a data-toggle="tab" href="#tab-13" aria-expanded="false" onclick="sqbGoodsInfo()">关联推荐商品</a>
</li>
</ul>
<div class="tab-content">
<div id="tab-1" class="tab-pane">
......@@ -373,6 +375,13 @@
height=800px frameborder=0></iframe>
</div>
</div>
<div id="tab-13" class="tab-pane">
<div class="panel-body">
<iframe id="sqb_goods_iframe" name="sqb_goods_iframe" marginwidth=0 marginheight=0
width=100%
height=800px frameborder=0></iframe>
</div>
</div>
</div>
</div>
</div>
......@@ -578,6 +587,11 @@
document.getElementById("artist_lineup_iframe").src = "../artistLineup/" + '[[${kylinPerformanceMisVo.performancesId}]]'.replaceAll("\"", "");
}
//关联推荐商品
function sqbGoodsInfo() {
document.getElementById("sqb_goods_iframe").src = ctx + "sqb/performance/goods/page/" + '[[${kylinPerformanceMisVo.performancesId}]]'.replaceAll("\"", "");
}
$("#tab-nav-1").bind("click", function () {
$("#tab_iframe_1").attr("src", prefix + "/performanceStatic/" + '[[${kylinPerformanceMisVo.performancesId}]]'.replaceAll("\"", ""));
});
......
package com.liquidnet.client.admin.zhengzai.goblin.dto;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 演出-商品关联绑定请求 DTO
*/
@Data
public class SqbPerfGoodsBindItemParam implements Serializable {
private static final long serialVersionUID = 1L;
/** SKU ID */
private String skuId;
/** 排序权重 */
private Integer sort;
/** 换购价格(null 表示不设置换购价,按原价售卖) */
private BigDecimal settlementPrice;
/** 演出结束自动下架 0-否 1-是 */
private Integer autoOffline;
}
package com.liquidnet.client.admin.zhengzai.goblin.service;
import com.liquidnet.client.admin.zhengzai.goblin.dto.SqbPerfGoodsBindItemParam;
import com.liquidnet.service.base.ResponseDto;
import com.liquidnet.service.goblin.dto.vo.GoblinSqbPerfGoodsVo;
......@@ -11,14 +12,13 @@ import java.util.List;
public interface ISqbPerformanceGoodsService {
/**
* 关联演出与商品(批量)
* 关联演出与商品(批量,支持换购价/自动下架配置
*
* @param performancesId 演出ID
* @param skuIds SKU ID 列表
* @param sort 排序权重
* @param items 绑定项列表(含配置)
* @return 操作结果
*/
ResponseDto<Boolean> bind(String performancesId, List<String> skuIds, Integer sort);
ResponseDto<Boolean> bind(String performancesId, List<SqbPerfGoodsBindItemParam> items);
/**
* 解除演出与商品关联
......@@ -36,4 +36,12 @@ public interface ISqbPerformanceGoodsService {
* @return 商品列表
*/
ResponseDto<List<GoblinSqbPerfGoodsVo>> list(String performancesId);
/**
* 搜索收钱吧商品(skuType=33,供关联候选)
*
* @param keyword 商品名称关键词
* @return 商品列表
*/
ResponseDto<List<GoblinSqbPerfGoodsVo>> searchGoods(String keyword);
}
......@@ -2,12 +2,11 @@ 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.dto.SqbPerfGoodsBindItemParam;
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.client.admin.zhengzai.goblin.utils.GoblinRedisUtils;
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;
......@@ -16,6 +15,7 @@ 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 org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
......@@ -33,8 +33,8 @@ import java.util.stream.Collectors;
@Service
public class SqbPerformanceGoodsServiceImpl implements ISqbPerformanceGoodsService {
/** Redis key 前缀,与 goblin C端接口保持一致 */
private static final String PERF_GOODS_CACHE_KEY_PREFIX = "goblin:sqb:perf:goods:";
/** skuType=33 代表收钱吧商品 */
private static final int SQB_SKU_TYPE = 33;
@Autowired
private GoblinSqbPerformanceGoodsMapper performanceGoodsMapper;
......@@ -46,27 +46,27 @@ public class SqbPerformanceGoodsServiceImpl implements ISqbPerformanceGoodsServi
private GoblinGoodsSkuMapper goblinGoodsSkuMapper;
@Autowired
private RedisDataSourceUtil redisDataSourceUtil;
private GoblinRedisUtils goblinRedisUtils;
@Override
public ResponseDto<Boolean> bind(String performancesId, List<String> skuIds, Integer sort) {
if (performancesId == null || performancesId.isEmpty()) {
public ResponseDto<Boolean> bind(String performancesId, List<SqbPerfGoodsBindItemParam> items) {
if (!StringUtils.hasText(performancesId)) {
return ResponseDto.failure("演出ID不能为空");
}
if (skuIds == null || skuIds.isEmpty()) {
return ResponseDto.failure("SKU列表不能为空");
if (items == null || items.isEmpty()) {
return ResponseDto.failure("商品列表不能为空");
}
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()) {
for (SqbPerfGoodsBindItemParam item : items) {
String skuId = item.getSkuId();
if (!StringUtils.hasText(skuId)) {
continue;
}
// 查询 SKU 获取 spuId
// 查询 SKU 获取 spuId,并校验 skuType=33
LambdaQueryWrapper<GoblinGoodsSku> skuQuery = new LambdaQueryWrapper<>();
skuQuery.eq(GoblinGoodsSku::getSkuId, skuId).last("LIMIT 1");
GoblinGoodsSku sku = goblinGoodsSkuMapper.selectOne(skuQuery);
......@@ -74,6 +74,13 @@ public class SqbPerformanceGoodsServiceImpl implements ISqbPerformanceGoodsServi
log.warn("[演出商品关联] SKU不存在,skuId={}", skuId);
continue;
}
if (sku.getSkuType() == null || sku.getSkuType() != SQB_SKU_TYPE) {
log.warn("[演出商品关联] 非收钱吧商品(skuType!=33),skuId={}", skuId);
continue;
}
int sortVal = item.getSort() != null ? item.getSort() : 0;
int autoOfflineVal = item.getAutoOffline() != null ? item.getAutoOffline() : 0;
// 检查是否已存在关联
LambdaQueryWrapper<GoblinSqbPerformanceGoods> existQuery = new LambdaQueryWrapper<>();
......@@ -83,12 +90,14 @@ public class SqbPerformanceGoodsServiceImpl implements ISqbPerformanceGoodsServi
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::getSettlementPrice, item.getSettlementPrice())
.set(GoblinSqbPerformanceGoods::getAutoOffline, autoOfflineVal)
.set(GoblinSqbPerformanceGoods::getUpdatedAt, now);
performanceGoodsMapper.update(null, updateWrapper);
} else {
......@@ -99,6 +108,8 @@ public class SqbPerformanceGoodsServiceImpl implements ISqbPerformanceGoodsServi
entity.setSkuId(skuId);
entity.setSort(sortVal);
entity.setStatus(1);
entity.setSettlementPrice(item.getSettlementPrice());
entity.setAutoOffline(autoOfflineVal);
entity.setCreatedAt(now);
entity.setUpdatedAt(now);
performanceGoodsMapper.insert(entity);
......@@ -106,7 +117,7 @@ public class SqbPerformanceGoodsServiceImpl implements ISqbPerformanceGoodsServi
}
// 删除 Redis 缓存,由 C端接口下次请求时重建
delPerfGoodsCache(performancesId);
goblinRedisUtils.delPerformanceGoodsCache(performancesId);
return ResponseDto.success(Boolean.TRUE);
} catch (Exception e) {
......@@ -117,10 +128,10 @@ public class SqbPerformanceGoodsServiceImpl implements ISqbPerformanceGoodsServi
@Override
public ResponseDto<Boolean> unbind(String performancesId, String skuId) {
if (performancesId == null || performancesId.isEmpty()) {
if (!StringUtils.hasText(performancesId)) {
return ResponseDto.failure("演出ID不能为空");
}
if (skuId == null || skuId.isEmpty()) {
if (!StringUtils.hasText(skuId)) {
return ResponseDto.failure("SKU ID不能为空");
}
......@@ -137,9 +148,7 @@ public class SqbPerformanceGoodsServiceImpl implements ISqbPerformanceGoodsServi
return ResponseDto.failure("关联记录不存在");
}
// 删除 Redis 缓存
delPerfGoodsCache(performancesId);
goblinRedisUtils.delPerformanceGoodsCache(performancesId);
return ResponseDto.success(Boolean.TRUE);
} catch (Exception e) {
log.error("[演出商品关联] unbind 异常,performancesId={}, skuId={}", performancesId, skuId, e);
......@@ -149,12 +158,11 @@ public class SqbPerformanceGoodsServiceImpl implements ISqbPerformanceGoodsServi
@Override
public ResponseDto<List<GoblinSqbPerfGoodsVo>> list(String performancesId) {
if (performancesId == null || performancesId.isEmpty()) {
if (!StringUtils.hasText(performancesId)) {
return ResponseDto.failure("演出ID不能为空");
}
try {
// 查询启用状态的关联记录
LambdaQueryWrapper<GoblinSqbPerformanceGoods> query = new LambdaQueryWrapper<>();
query.eq(GoblinSqbPerformanceGoods::getPerformancesId, performancesId)
.eq(GoblinSqbPerformanceGoods::getStatus, 1)
......@@ -165,47 +173,32 @@ public class SqbPerformanceGoodsServiceImpl implements ISqbPerformanceGoodsServi
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 信息
.map(GoblinSqbPerformanceGoods::getSkuId).collect(Collectors.toList());
// 因为收钱吧商品 SKU 已经包含了名称和图片等全部信息,无需再去关联查询 SPU 表
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));
Map<String, GoblinGoodsSku> skuMap = goblinGoodsSkuMapper.selectList(skuQuery)
.stream().collect(Collectors.toMap(GoblinGoodsSku::getSkuId, 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());
vo.setSettlementPrice(rel.getSettlementPrice());
vo.setAutoOffline(rel.getAutoOffline());
vo.setStatus(rel.getStatus());
GoblinGoodsSku sku = skuMap.get(rel.getSkuId());
if (sku != null) {
vo.setSkuName(sku.getName());
vo.setSpuName(sku.getName()); // 取代原本从 SPU 获取名称,直接用 SKU 名称
vo.setCoverPic(sku.getSkuPic()); // 统一使用 SKU 上的默认图片(即原 SPU coverPic)
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());
vo.setStock(sku.getSkuStock());
}
result.add(vo);
......@@ -218,16 +211,39 @@ public class SqbPerformanceGoodsServiceImpl implements ISqbPerformanceGoodsServi
}
}
/**
* 删除演出关联商品的 Redis 缓存
*/
private void delPerfGoodsCache(String performancesId) {
@Override
public ResponseDto<List<GoblinSqbPerfGoodsVo>> searchGoods(String keyword) {
try {
String cacheKey = PERF_GOODS_CACHE_KEY_PREFIX + performancesId;
redisDataSourceUtil.getRedisGoblinUtil().del(cacheKey);
log.info("[演出商品关联] 已删除 Redis 缓存,key={}", cacheKey);
// 收钱吧商品的所有核心展示信息均已同步到 SKU(Type=33)表中,直接一次查询避免二次查 SPU
LambdaQueryWrapper<GoblinGoodsSku> skuQuery = new LambdaQueryWrapper<>();
skuQuery.eq(GoblinGoodsSku::getSkuType, SQB_SKU_TYPE);
if (StringUtils.hasText(keyword)) {
skuQuery.like(GoblinGoodsSku::getName, keyword);
}
skuQuery.last("LIMIT 50");
List<GoblinGoodsSku> skuList = goblinGoodsSkuMapper.selectList(skuQuery);
if (skuList.isEmpty()) {
return ResponseDto.success(new ArrayList<>());
}
List<GoblinSqbPerfGoodsVo> result = new ArrayList<>();
for (GoblinGoodsSku sku : skuList) {
GoblinSqbPerfGoodsVo vo = new GoblinSqbPerfGoodsVo();
vo.setSkuId(sku.getSkuId());
vo.setSpuId(sku.getSpuId());
vo.setSkuName(sku.getName());
vo.setSpuName(sku.getName()); // 前端显示需要,统一赋值为 sku 名称
vo.setCoverPic(sku.getSkuPic()); // 同步阶段已将图片存入了 skuPic
vo.setPrice(sku.getPrice() != null ? sku.getPrice().longValue() : null);
vo.setStock(sku.getSkuStock());
result.add(vo);
}
return ResponseDto.success(result);
} catch (Exception e) {
log.warn("[演出商品关联] 删除 Redis 缓存失败,performancesId={}", performancesId, e);
log.error("[演出商品关联] searchGoods 异常,keyword={}", keyword, e);
return ResponseDto.failure("搜索失败:" + e.getMessage());
}
}
}
......@@ -249,4 +249,14 @@ public class GoblinRedisUtils {
rk = rk.concat(skuId);
return (int) redisDataSourceUtil.getRedisGoblinUtil().incr(rk, stock);
}
/**
* 清除演出关联收钱吧商品缓存
*
* @param performancesId 演出ID
*/
public void delPerformanceGoodsCache(String performancesId) {
String redisKey = GoblinRedisConst.SQB_PERFORMANCE_GOODS.concat(performancesId);
redisDataSourceUtil.getRedisGoblinUtil().del(redisKey);
}
}
......@@ -56,4 +56,19 @@ public class RedisKeyExpireConst {
// 演出关联阵容缓存过期时间
public static final long PERFORMANCES_ARTISTS_EXPIRE = 30 * 24 * 60 * 60;
/**
* 演出关联收钱吧商品缓存过期时间 (5分钟)
*/
public static final long SQB_PERFORMANCE_GOODS_EXPIRE = 5 * 60;
/**
* 收钱吧订单详情过期时间 (2小时)
*/
public static final long SQB_ORDER_EXPIRE = 2 * 60 * 60;
/**
* 收钱吧下单防重锁过期时间 (10秒)
*/
public static final long SQB_ORDER_LOCK_EXPIRE = 10;
}
......@@ -7,6 +7,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* <p>
......@@ -46,6 +47,16 @@ public class GoblinSqbPerformanceGoods implements Serializable {
*/
private Integer sort;
/**
* 换购价格(不设置则按商品售价售卖)
*/
private BigDecimal settlementPrice;
/**
* 演出结束自动下架 0-否 1-是
*/
private Integer autoOffline;
/**
* 状态 0-禁用 1-启用
*/
......
package com.liquidnet.service.goblin.util;
import com.fasterxml.jackson.core.type.TypeReference;
import com.liquidnet.service.goblin.constant.GoblinRedisConst;
import com.liquidnet.service.base.constant.RedisKeyExpireConst;
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;
......@@ -19,53 +18,35 @@ import java.util.List;
@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);
redisUtil.set(GoblinRedisConst.SQB_ORDER.concat(orderId), vo, RedisKeyExpireConst.SQB_ORDER_EXPIRE);
}
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);
return (GoblinSqbOrderVo) redisUtil.get(GoblinRedisConst.SQB_ORDER.concat(orderId));
}
public void delSqbOrder(String orderId) {
redisUtil.del(KEY_SQB_ORDER.concat(orderId));
redisUtil.del(GoblinRedisConst.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);
redisUtil.set(GoblinRedisConst.SQB_PERFORMANCE_GOODS.concat(performancesId), list, RedisKeyExpireConst.SQB_PERFORMANCE_GOODS_EXPIRE);
}
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>>() {});
return (List<GoblinSqbPerfGoodsVo>) redisUtil.get(GoblinRedisConst.SQB_PERFORMANCE_GOODS.concat(performancesId));
}
public void delPerfGoods(String performancesId) {
redisUtil.del(KEY_SQB_PERF_GOODS.concat(performancesId));
redisUtil.del(GoblinRedisConst.SQB_PERFORMANCE_GOODS.concat(performancesId));
}
/* ---------------------------------------- 下单防重锁(TTL 10s) ---------------------------------------- */
......@@ -78,8 +59,8 @@ public class GoblinSqbRedisUtils {
* @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);
String key = GoblinRedisConst.SQB_ORDER_LOCK.concat(userId).concat(":").concat(skuId);
return redisUtil.lock(key, 1, RedisKeyExpireConst.SQB_ORDER_LOCK_EXPIRE);
}
/**
......@@ -89,7 +70,7 @@ public class GoblinSqbRedisUtils {
* @param skuId SKU ID
*/
public void releaseOrderLock(String userId, String skuId) {
String key = KEY_SQB_ORDER_LOCK.concat(userId).concat(":").concat(skuId);
String key = GoblinRedisConst.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