Compare commits
17 Commits
51d3a029cc
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1536f1780b | |||
| 804a5a7bef | |||
| 60279fca4c | |||
| 2c076e2b0f | |||
| 48cd2e1f7b | |||
| 88afd149c3 | |||
| 9672be2252 | |||
| 1107b9144f | |||
| acc543b50a | |||
| b9c70bb4a3 | |||
| ee9ea88ce9 | |||
| 093826435e | |||
| 85a8d17194 | |||
| bbd41da1d3 | |||
| e4e10d46cc | |||
| 195e90df5e | |||
| c5da6f371b |
@@ -1,125 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
###############################################################################
|
||||
# 捐款证书中文乱码修复脚本
|
||||
# 用途:在运行中的Docker容器内安装中文字体
|
||||
# 适用于:无法重新构建镜像的紧急情况
|
||||
###############################################################################
|
||||
|
||||
set -e # 遇到错误立即退出
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 容器名称(可根据实际情况修改)
|
||||
CONTAINER_NAME="websoft-api-container"
|
||||
|
||||
echo -e "${GREEN}================================${NC}"
|
||||
echo -e "${GREEN}中文字体修复脚本${NC}"
|
||||
echo -e "${GREEN}================================${NC}"
|
||||
echo ""
|
||||
|
||||
# 检查容器是否存在
|
||||
echo -e "${YELLOW}步骤 1/6: 检查容器状态...${NC}"
|
||||
if ! docker ps | grep -q "$CONTAINER_NAME"; then
|
||||
echo -e "${RED}错误:容器 $CONTAINER_NAME 未运行!${NC}"
|
||||
echo "当前运行的容器:"
|
||||
docker ps --format "table {{.Names}}\t{{.Status}}"
|
||||
echo ""
|
||||
read -p "请输入正确的容器名称: " CONTAINER_NAME
|
||||
if [ -z "$CONTAINER_NAME" ]; then
|
||||
echo -e "${RED}容器名称不能为空,退出。${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo -e "${GREEN}✓ 容器正在运行${NC}"
|
||||
echo ""
|
||||
|
||||
# 检查是否已安装字体
|
||||
echo -e "${YELLOW}步骤 2/6: 检查是否已安装中文字体...${NC}"
|
||||
if docker exec "$CONTAINER_NAME" fc-list :lang=zh 2>/dev/null | grep -q "WenQuanYi"; then
|
||||
echo -e "${GREEN}✓ 中文字体已安装${NC}"
|
||||
docker exec "$CONTAINER_NAME" fc-list :lang=zh
|
||||
echo ""
|
||||
read -p "是否重新安装?(y/N): " REINSTALL
|
||||
if [[ ! "$REINSTALL" =~ ^[Yy]$ ]]; then
|
||||
echo "跳过安装,退出。"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}未检测到中文字体,开始安装...${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 安装字体工具
|
||||
echo -e "${YELLOW}步骤 3/6: 安装字体工具...${NC}"
|
||||
docker exec -u root "$CONTAINER_NAME" sh -c "apk add --no-cache fontconfig ttf-dejavu wget" || {
|
||||
echo -e "${RED}✗ 字体工具安装失败${NC}"
|
||||
exit 1
|
||||
}
|
||||
echo -e "${GREEN}✓ 字体工具安装成功${NC}"
|
||||
echo ""
|
||||
|
||||
# 下载中文字体
|
||||
echo -e "${YELLOW}步骤 4/6: 下载文泉驿微米黑字体...${NC}"
|
||||
echo "正在从GitHub下载(约10MB),请稍候..."
|
||||
docker exec -u root "$CONTAINER_NAME" sh -c "
|
||||
wget -O /tmp/wqy-microhei.ttc https://github.com/anthonyfok/fonts-wqy-microhei/raw/master/wqy-microhei.ttc 2>&1 | grep -E 'Connecting|Length|saved' || true
|
||||
" || {
|
||||
echo -e "${YELLOW}GitHub下载失败,尝试使用代理...${NC}"
|
||||
docker exec -u root "$CONTAINER_NAME" sh -c "
|
||||
wget -O /tmp/wqy-microhei.ttc https://ghproxy.com/https://github.com/anthonyfok/fonts-wqy-microhei/raw/master/wqy-microhei.ttc
|
||||
" || {
|
||||
echo -e "${RED}✗ 字体下载失败${NC}"
|
||||
echo "请检查网络连接或手动下载字体文件。"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
echo -e "${GREEN}✓ 字体下载成功${NC}"
|
||||
echo ""
|
||||
|
||||
# 安装字体
|
||||
echo -e "${YELLOW}步骤 5/6: 安装字体文件...${NC}"
|
||||
docker exec -u root "$CONTAINER_NAME" sh -c "
|
||||
mkdir -p /usr/share/fonts/truetype/wqy && \
|
||||
mv /tmp/wqy-microhei.ttc /usr/share/fonts/truetype/wqy/ && \
|
||||
fc-cache -fv
|
||||
" || {
|
||||
echo -e "${RED}✗ 字体安装失败${NC}"
|
||||
exit 1
|
||||
}
|
||||
echo -e "${GREEN}✓ 字体安装成功${NC}"
|
||||
echo ""
|
||||
|
||||
# 验证安装
|
||||
echo -e "${YELLOW}步骤 6/6: 验证字体安装...${NC}"
|
||||
FONT_COUNT=$(docker exec "$CONTAINER_NAME" fc-list :lang=zh | wc -l)
|
||||
if [ "$FONT_COUNT" -gt 0 ]; then
|
||||
echo -e "${GREEN}✓ 中文字体验证成功!${NC}"
|
||||
echo "已安装的中文字体:"
|
||||
docker exec "$CONTAINER_NAME" fc-list :lang=zh
|
||||
else
|
||||
echo -e "${RED}✗ 字体验证失败${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 完成提示
|
||||
echo -e "${GREEN}================================${NC}"
|
||||
echo -e "${GREEN}修复完成!${NC}"
|
||||
echo -e "${GREEN}================================${NC}"
|
||||
echo ""
|
||||
echo "后续步骤:"
|
||||
echo "1. 不需要重启容器,字体已生效"
|
||||
echo "2. 重新生成捐款证书即可看到效果"
|
||||
echo "3. 如果仍有问题,请查看容器日志:"
|
||||
echo " docker logs -f $CONTAINER_NAME"
|
||||
echo ""
|
||||
echo -e "${YELLOW}注意:${NC}"
|
||||
echo "- 此修复方法在容器重启后会失效"
|
||||
echo "- 建议后续使用更新后的Dockerfile重新构建镜像"
|
||||
echo "- 详细文档请参考:docs/chinese-font-fix-guide.md"
|
||||
echo ""
|
||||
@@ -337,9 +337,16 @@ public class CmsWebsiteServiceImpl extends ServiceImpl<CmsWebsiteMapper, CmsWebs
|
||||
if (StrUtil.isNotBlank(siteInfo)) {
|
||||
log.info("从缓存获取网站信息,租户ID: {}", tenantId);
|
||||
try {
|
||||
return JSONUtil.parseObject(siteInfo, ShopVo.class);
|
||||
// 兼容历史缓存:JSON "null" 会被解析为 null;此时应视为未命中并回源数据库。
|
||||
ShopVo cacheVo = JSONUtil.parseObject(siteInfo, ShopVo.class);
|
||||
if (cacheVo != null) {
|
||||
return cacheVo;
|
||||
}
|
||||
log.warn("网站信息缓存命中但内容为空(null),清理缓存后回源数据库,租户ID: {}", tenantId);
|
||||
redisUtil.delete(cacheKey);
|
||||
} catch (Exception e) {
|
||||
log.warn("缓存解析失败,从数据库重新获取: {}", e.getMessage());
|
||||
log.warn("缓存解析失败,清理缓存后从数据库重新获取: {}", e.getMessage());
|
||||
redisUtil.delete(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package cn.afterturn.easypoi.excel.entity;
|
||||
package com.gxwebsoft.common.core.web;
|
||||
|
||||
import cn.afterturn.easypoi.excel.entity.ExcelBaseParams;
|
||||
import cn.afterturn.easypoi.handler.inter.IExcelVerifyHandler;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.gxwebsoft.common.system.service;
|
||||
|
||||
/**
|
||||
* 微信小程序 access_token 获取服务(按租户)。
|
||||
*
|
||||
* <p>用于调用微信小程序开放接口(例如:上传发货信息)。</p>
|
||||
*/
|
||||
public interface WxMiniappAccessTokenService {
|
||||
|
||||
/**
|
||||
* 获取指定租户的小程序 access_token(内部带缓存)。
|
||||
*
|
||||
* @param tenantId 租户ID
|
||||
* @return access_token
|
||||
*/
|
||||
String getAccessToken(Integer tenantId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.gxwebsoft.common.system.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||
import com.gxwebsoft.common.system.service.WxMiniappAccessTokenService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.gxwebsoft.common.core.constants.RedisConstants.ACCESS_TOKEN_KEY;
|
||||
import static com.gxwebsoft.common.core.constants.RedisConstants.MP_WX_KEY;
|
||||
|
||||
/**
|
||||
* 微信小程序 access_token 获取实现(按租户)。
|
||||
*
|
||||
* <p>复用现有缓存结构:
|
||||
* <ul>
|
||||
* <li>小程序配置:Redis key = {@code mp-weixin:{tenantId}},value 为 JSON,包含 appId/appSecret</li>
|
||||
* <li>access_token:Redis key = {@code access-token:{tenantId}},value 为微信返回的 JSON 字符串</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class WxMiniappAccessTokenServiceImpl implements WxMiniappAccessTokenService {
|
||||
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@Override
|
||||
public String getAccessToken(Integer tenantId) {
|
||||
if (tenantId == null) {
|
||||
throw new BusinessException("tenantId 不能为空");
|
||||
}
|
||||
|
||||
final String tokenCacheKey = ACCESS_TOKEN_KEY + ":" + tenantId;
|
||||
|
||||
// 1) 优先从缓存取(兼容 JSON 或纯字符串 token 的历史格式)
|
||||
String cachedValue = redisUtil.get(tokenCacheKey);
|
||||
if (StrUtil.isNotBlank(cachedValue)) {
|
||||
try {
|
||||
JSONObject cachedJson = JSON.parseObject(cachedValue);
|
||||
String accessToken = cachedJson.getString("access_token");
|
||||
if (StrUtil.isNotBlank(accessToken)) {
|
||||
return accessToken;
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
// 旧格式:直接存 token
|
||||
return cachedValue;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 缓存没有则从租户配置获取 appId/appSecret
|
||||
final String wxConfigKey = MP_WX_KEY + tenantId;
|
||||
final String wxConfigValue = redisUtil.get(wxConfigKey);
|
||||
if (StrUtil.isBlank(wxConfigValue)) {
|
||||
throw new BusinessException("未找到微信小程序配置,请检查缓存key: " + wxConfigKey);
|
||||
}
|
||||
|
||||
JSONObject wxConfig;
|
||||
try {
|
||||
wxConfig = JSON.parseObject(wxConfigValue);
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("微信小程序配置格式错误: " + e.getMessage());
|
||||
}
|
||||
|
||||
final String appId = wxConfig.getString("appId");
|
||||
final String appSecret = wxConfig.getString("appSecret");
|
||||
if (StrUtil.isBlank(appId) || StrUtil.isBlank(appSecret)) {
|
||||
throw new BusinessException("微信小程序配置不完整(appId/appSecret)");
|
||||
}
|
||||
|
||||
// 3) 调用微信接口获取 token
|
||||
final String apiUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"
|
||||
+ "&appid=" + appId + "&secret=" + appSecret;
|
||||
String result = HttpUtil.get(apiUrl);
|
||||
|
||||
JSONObject json = JSON.parseObject(result);
|
||||
if (json.containsKey("errcode") && json.getIntValue("errcode") != 0) {
|
||||
Integer errcode = json.getInteger("errcode");
|
||||
String errmsg = json.getString("errmsg");
|
||||
throw new BusinessException("获取小程序access_token失败: " + errmsg + " (errcode: " + errcode + ")");
|
||||
}
|
||||
|
||||
String accessToken = json.getString("access_token");
|
||||
Integer expiresIn = json.getInteger("expires_in");
|
||||
if (StrUtil.isBlank(accessToken)) {
|
||||
throw new BusinessException("获取小程序access_token失败: access_token为空");
|
||||
}
|
||||
|
||||
// 4) 缓存微信原始 JSON(与现有实现保持一致),提前5分钟过期
|
||||
long ttlSeconds = 7000L;
|
||||
if (expiresIn != null && expiresIn > 300) {
|
||||
ttlSeconds = expiresIn - 300L;
|
||||
}
|
||||
redisUtil.set(tokenCacheKey, result, ttlSeconds, TimeUnit.SECONDS);
|
||||
|
||||
log.info("获取小程序access_token成功 - tenantId={}, ttlSeconds={}", tenantId, ttlSeconds);
|
||||
return accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.gxwebsoft.glt.controller;
|
||||
|
||||
import com.gxwebsoft.common.core.annotation.OperationLog;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.BaseController;
|
||||
import com.gxwebsoft.common.core.web.BatchParam;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||
import com.gxwebsoft.glt.param.GltTicketOrderParam;
|
||||
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 送水订单控制器
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2026-02-05 18:50:21
|
||||
*/
|
||||
@Tag(name = "送水订单管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/glt/glt-ticket-order")
|
||||
public class GltTicketOrderController extends BaseController {
|
||||
@Resource
|
||||
private GltTicketOrderService gltTicketOrderService;
|
||||
|
||||
@Operation(summary = "分页查询送水订单")
|
||||
@GetMapping("/page")
|
||||
public ApiResult<PageResult<GltTicketOrder>> page(GltTicketOrderParam param) {
|
||||
// 使用关联查询
|
||||
return success(gltTicketOrderService.pageRel(param));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询全部送水订单")
|
||||
@GetMapping()
|
||||
public ApiResult<List<GltTicketOrder>> list(GltTicketOrderParam param) {
|
||||
// 使用关联查询
|
||||
return success(gltTicketOrderService.listRel(param));
|
||||
}
|
||||
|
||||
@Operation(summary = "根据id查询送水订单")
|
||||
@GetMapping("/{id}")
|
||||
public ApiResult<GltTicketOrder> get(@PathVariable("id") Integer id) {
|
||||
// 使用关联查询
|
||||
return success(gltTicketOrderService.getByIdRel(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "添加送水订单")
|
||||
@PostMapping()
|
||||
public ApiResult<?> save(@RequestBody GltTicketOrder gltTicketOrder) {
|
||||
// 下单:后端原子完成(扣水票 + 写核销记录 + 生成订单)
|
||||
User loginUser = getLoginUser();
|
||||
if (loginUser == null) {
|
||||
return fail("请先登录");
|
||||
}
|
||||
gltTicketOrderService.createWithWriteOff(gltTicketOrder, loginUser.getUserId(), loginUser.getTenantId());
|
||||
return success("下单成功");
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('glt:gltTicketOrder:update')")
|
||||
@OperationLog
|
||||
@Operation(summary = "修改送水订单")
|
||||
@PutMapping()
|
||||
public ApiResult<?> update(@RequestBody GltTicketOrder gltTicketOrder) {
|
||||
if (gltTicketOrderService.updateById(gltTicketOrder)) {
|
||||
return success("修改成功");
|
||||
}
|
||||
return fail("修改失败");
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('glt:gltTicketOrder:remove')")
|
||||
@OperationLog
|
||||
@Operation(summary = "删除送水订单")
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResult<?> remove(@PathVariable("id") Integer id) {
|
||||
if (gltTicketOrderService.removeById(id)) {
|
||||
return success("删除成功");
|
||||
}
|
||||
return fail("删除失败");
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('glt:gltTicketOrder:save')")
|
||||
@OperationLog
|
||||
@Operation(summary = "批量添加送水订单")
|
||||
@PostMapping("/batch")
|
||||
public ApiResult<?> saveBatch(@RequestBody List<GltTicketOrder> list) {
|
||||
if (gltTicketOrderService.saveBatch(list)) {
|
||||
return success("添加成功");
|
||||
}
|
||||
return fail("添加失败");
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('glt:gltTicketOrder:update')")
|
||||
@OperationLog
|
||||
@Operation(summary = "批量修改送水订单")
|
||||
@PutMapping("/batch")
|
||||
public ApiResult<?> removeBatch(@RequestBody BatchParam<GltTicketOrder> batchParam) {
|
||||
if (batchParam.update(gltTicketOrderService, "id")) {
|
||||
return success("修改成功");
|
||||
}
|
||||
return fail("修改失败");
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('glt:gltTicketOrder:remove')")
|
||||
@OperationLog
|
||||
@Operation(summary = "批量删除送水订单")
|
||||
@DeleteMapping("/batch")
|
||||
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
|
||||
if (gltTicketOrderService.removeByIds(ids)) {
|
||||
return success("删除成功");
|
||||
}
|
||||
return fail("删除失败");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
package com.gxwebsoft.glt.controller;
|
||||
|
||||
import com.gxwebsoft.common.core.web.BaseController;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.glt.service.GltUserTicketService;
|
||||
import com.gxwebsoft.glt.entity.GltUserTicket;
|
||||
import com.gxwebsoft.glt.param.GltUserTicketParam;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.common.core.web.PageParam;
|
||||
import com.gxwebsoft.common.core.web.BatchParam;
|
||||
import com.gxwebsoft.common.core.annotation.OperationLog;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -16,7 +14,9 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 我的水票控制器
|
||||
@@ -38,6 +38,26 @@ public class GltUserTicketController extends BaseController {
|
||||
return success(gltUserTicketService.pageRel(param));
|
||||
}
|
||||
|
||||
@Operation(summary = "可用水票总数")
|
||||
@GetMapping("/my-total")
|
||||
public ApiResult<?> myTotal() {
|
||||
Integer userId = getLoginUserId();
|
||||
if (userId == null) {
|
||||
return fail("未登录");
|
||||
}
|
||||
Integer tenantId = getTenantId();
|
||||
if (tenantId == null) {
|
||||
return fail("租户信息缺失");
|
||||
}
|
||||
Integer availableQty = gltUserTicketService.sumAvailableQtyByUserId(userId, tenantId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("userId", userId);
|
||||
// 兼容旧字段:totalQty 表示“可用水票总数”
|
||||
data.put("totalQty", availableQty);
|
||||
data.put("availableQty", availableQty);
|
||||
return success(data);
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('glt:gltUserTicket:list')")
|
||||
@Operation(summary = "查询全部我的水票")
|
||||
@GetMapping()
|
||||
|
||||
@@ -55,8 +55,6 @@ public class GltUserTicketLogController extends BaseController {
|
||||
return success(gltUserTicketLogService.getByIdRel(id));
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('glt:gltUserTicketLog:save')")
|
||||
@OperationLog
|
||||
@Operation(summary = "添加消费日志")
|
||||
@PostMapping()
|
||||
public ApiResult<?> save(@RequestBody GltUserTicketLog gltUserTicketLog) {
|
||||
|
||||
88
src/main/java/com/gxwebsoft/glt/entity/GltTicketOrder.java
Normal file
88
src/main/java/com/gxwebsoft/glt/entity/GltTicketOrder.java
Normal file
@@ -0,0 +1,88 @@
|
||||
package com.gxwebsoft.glt.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 送水订单
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2026-02-05 18:50:20
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(name = "GltTicketOrder对象", description = "送水订单")
|
||||
public class GltTicketOrder implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Integer id;
|
||||
|
||||
@Schema(description = "用户水票ID")
|
||||
private Integer userTicketId;
|
||||
|
||||
@Schema(description = "门店ID")
|
||||
private Integer storeId;
|
||||
|
||||
@Schema(description = "配送员")
|
||||
private Integer riderId;
|
||||
|
||||
@Schema(description = "仓库ID")
|
||||
private Integer warehouseId;
|
||||
|
||||
@Schema(description = "关联收货地址")
|
||||
private Integer addressId;
|
||||
|
||||
@Schema(description = "收货地址")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "买家留言")
|
||||
private String buyerRemarks;
|
||||
|
||||
@Schema(description = "用于统计")
|
||||
private BigDecimal price;
|
||||
|
||||
@Schema(description = "购买数量")
|
||||
private Integer totalNum;
|
||||
|
||||
@Schema(description = "配送时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private String sendTime;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Integer userId;
|
||||
|
||||
@Schema(description = "排序(数字越小越靠前)")
|
||||
private Integer sortNumber;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String comments;
|
||||
|
||||
@Schema(description = "状态, 0正常, 1冻结")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "是否删除, 0否, 1是")
|
||||
@TableLogic
|
||||
private Integer deleted;
|
||||
|
||||
@Schema(description = "租户id")
|
||||
private Integer tenantId;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "修改时间")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.gxwebsoft.glt.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||
import com.gxwebsoft.glt.param.GltTicketOrderParam;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 送水订单Mapper
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2026-02-05 18:50:20
|
||||
*/
|
||||
public interface GltTicketOrderMapper extends BaseMapper<GltTicketOrder> {
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param param 查询参数
|
||||
* @return List<GltTicketOrder>
|
||||
*/
|
||||
List<GltTicketOrder> selectPageRel(@Param("page") IPage<GltTicketOrder> page,
|
||||
@Param("param") GltTicketOrderParam param);
|
||||
|
||||
/**
|
||||
* 查询全部
|
||||
*
|
||||
* @param param 查询参数
|
||||
* @return List<User>
|
||||
*/
|
||||
List<GltTicketOrder> selectListRel(@Param("param") GltTicketOrderParam param);
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.gxwebsoft.glt.entity.GltUserTicket;
|
||||
import com.gxwebsoft.glt.param.GltUserTicketParam;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
@@ -34,4 +35,32 @@ public interface GltUserTicketMapper extends BaseMapper<GltUserTicket> {
|
||||
*/
|
||||
List<GltUserTicket> selectListRel(@Param("param") GltUserTicketParam param);
|
||||
|
||||
/**
|
||||
* 统计用户可用水票总数(sum(available_qty))
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID
|
||||
* @return 可用总数
|
||||
*/
|
||||
Integer sumAvailableQtyByUserId(@Param("userId") Integer userId,
|
||||
@Param("tenantId") Integer tenantId);
|
||||
|
||||
/**
|
||||
* 按当前用户锁定水票记录(用于扣减/核销的事务场景)
|
||||
*/
|
||||
@Select("""
|
||||
SELECT *
|
||||
FROM glt_user_ticket
|
||||
WHERE id = #{id}
|
||||
AND user_id = #{userId}
|
||||
AND tenant_id = #{tenantId}
|
||||
AND status = 0
|
||||
AND deleted = 0
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
""")
|
||||
GltUserTicket selectByIdForUpdate(@Param("id") Integer id,
|
||||
@Param("userId") Integer userId,
|
||||
@Param("tenantId") Integer tenantId);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.gxwebsoft.glt.mapper.GltTicketOrderMapper">
|
||||
|
||||
<!-- 关联查询sql -->
|
||||
<sql id="selectSql">
|
||||
SELECT a.*
|
||||
FROM glt_ticket_order a
|
||||
<where>
|
||||
<if test="param.id != null">
|
||||
AND a.id = #{param.id}
|
||||
</if>
|
||||
<if test="param.userTicketId != null">
|
||||
AND a.user_ticket_id = #{param.userTicketId}
|
||||
</if>
|
||||
<if test="param.storeId != null">
|
||||
AND a.store_id = #{param.storeId}
|
||||
</if>
|
||||
<if test="param.riderId != null">
|
||||
AND a.rider_id = #{param.riderId}
|
||||
</if>
|
||||
<if test="param.warehouseId != null">
|
||||
AND a.warehouse_id = #{param.warehouseId}
|
||||
</if>
|
||||
<if test="param.addressId != null">
|
||||
AND a.address_id = #{param.addressId}
|
||||
</if>
|
||||
<if test="param.address != null">
|
||||
AND a.address LIKE CONCAT('%', #{param.address}, '%')
|
||||
</if>
|
||||
<if test="param.buyerRemarks != null">
|
||||
AND a.buyer_remarks LIKE CONCAT('%', #{param.buyerRemarks}, '%')
|
||||
</if>
|
||||
<if test="param.price != null">
|
||||
AND a.price = #{param.price}
|
||||
</if>
|
||||
<if test="param.totalNum != null">
|
||||
AND a.total_num = #{param.totalNum}
|
||||
</if>
|
||||
<if test="param.userId != null">
|
||||
AND a.user_id = #{param.userId}
|
||||
</if>
|
||||
<if test="param.sortNumber != null">
|
||||
AND a.sort_number = #{param.sortNumber}
|
||||
</if>
|
||||
<if test="param.comments != null">
|
||||
AND a.comments LIKE CONCAT('%', #{param.comments}, '%')
|
||||
</if>
|
||||
<if test="param.status != null">
|
||||
AND a.status = #{param.status}
|
||||
</if>
|
||||
<if test="param.deleted != null">
|
||||
AND a.deleted = #{param.deleted}
|
||||
</if>
|
||||
<if test="param.deleted == null">
|
||||
AND a.deleted = 0
|
||||
</if>
|
||||
<if test="param.createTimeStart != null">
|
||||
AND a.create_time >= #{param.createTimeStart}
|
||||
</if>
|
||||
<if test="param.createTimeEnd != null">
|
||||
AND a.create_time <= #{param.createTimeEnd}
|
||||
</if>
|
||||
<if test="param.keywords != null">
|
||||
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
)
|
||||
</if>
|
||||
</where>
|
||||
</sql>
|
||||
|
||||
<!-- 分页查询 -->
|
||||
<select id="selectPageRel" resultType="com.gxwebsoft.glt.entity.GltTicketOrder">
|
||||
<include refid="selectSql"></include>
|
||||
</select>
|
||||
|
||||
<!-- 查询全部 -->
|
||||
<select id="selectListRel" resultType="com.gxwebsoft.glt.entity.GltTicketOrder">
|
||||
<include refid="selectSql"></include>
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -4,11 +4,12 @@
|
||||
|
||||
<!-- 关联查询sql -->
|
||||
<sql id="selectSql">
|
||||
SELECT a.*, u.nickname, u.avatar, u.phone, m.name AS templateName, o.pay_price
|
||||
SELECT a.*, u.nickname, u.avatar, u.phone, m.name AS templateName, o.pay_price AS payPrice
|
||||
FROM glt_user_ticket a
|
||||
LEFT JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id
|
||||
LEFT JOIN glt_ticket_template m ON a.template_id = m.id
|
||||
LEFT JOIN shop_order o ON a.order_no = o.order_no
|
||||
<!-- 使用 order_id + tenant_id 关联,避免 order_no 跨租户/重复导致 a.id 数据被 JOIN 放大 -->
|
||||
LEFT JOIN shop_order o ON a.order_id = o.order_id AND a.tenant_id = o.tenant_id AND o.deleted = 0
|
||||
<where>
|
||||
<if test="param.id != null">
|
||||
AND a.id = #{param.id}
|
||||
@@ -86,4 +87,14 @@
|
||||
<include refid="selectSql"></include>
|
||||
</select>
|
||||
|
||||
<!-- 我的可用水票总数(sum(available_qty)) -->
|
||||
<select id="sumAvailableQtyByUserId" resultType="java.lang.Integer">
|
||||
SELECT IFNULL(SUM(available_qty), 0)
|
||||
FROM glt_user_ticket
|
||||
WHERE user_id = #{userId}
|
||||
AND tenant_id = #{tenantId}
|
||||
AND status = 0
|
||||
AND deleted = 0
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.gxwebsoft.glt.param;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.gxwebsoft.common.core.annotation.QueryField;
|
||||
import com.gxwebsoft.common.core.annotation.QueryType;
|
||||
import com.gxwebsoft.common.core.web.BaseParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 送水订单查询参数
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2026-02-05 18:50:19
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Schema(name = "GltTicketOrderParam对象", description = "送水订单查询参数")
|
||||
public class GltTicketOrderParam extends BaseParam {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@QueryField(type = QueryType.EQ)
|
||||
private Integer id;
|
||||
|
||||
@Schema(description = "用户水票ID")
|
||||
@QueryField(type = QueryType.EQ)
|
||||
private Integer userTicketId;
|
||||
|
||||
@Schema(description = "门店ID")
|
||||
@QueryField(type = QueryType.EQ)
|
||||
private Integer storeId;
|
||||
|
||||
@Schema(description = "配送员")
|
||||
@QueryField(type = QueryType.EQ)
|
||||
private Integer riderId;
|
||||
|
||||
@Schema(description = "仓库ID")
|
||||
@QueryField(type = QueryType.EQ)
|
||||
private Integer warehouseId;
|
||||
|
||||
@Schema(description = "关联收货地址")
|
||||
@QueryField(type = QueryType.EQ)
|
||||
private Integer addressId;
|
||||
|
||||
@Schema(description = "收货地址")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "买家留言")
|
||||
private String buyerRemarks;
|
||||
|
||||
@Schema(description = "用于统计")
|
||||
@QueryField(type = QueryType.EQ)
|
||||
private BigDecimal price;
|
||||
|
||||
@Schema(description = "购买数量")
|
||||
@QueryField(type = QueryType.EQ)
|
||||
private Integer totalNum;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
@QueryField(type = QueryType.EQ)
|
||||
private Integer userId;
|
||||
|
||||
@Schema(description = "排序(数字越小越靠前)")
|
||||
@QueryField(type = QueryType.EQ)
|
||||
private Integer sortNumber;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String comments;
|
||||
|
||||
@Schema(description = "状态, 0正常, 1冻结")
|
||||
@QueryField(type = QueryType.EQ)
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "是否删除, 0否, 1是")
|
||||
@QueryField(type = QueryType.EQ)
|
||||
private Integer deleted;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.gxwebsoft.glt.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||
import com.gxwebsoft.glt.param.GltTicketOrderParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 送水订单Service
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2026-02-05 18:50:20
|
||||
*/
|
||||
public interface GltTicketOrderService extends IService<GltTicketOrder> {
|
||||
|
||||
/**
|
||||
* 分页关联查询
|
||||
*
|
||||
* @param param 查询参数
|
||||
* @return PageResult<GltTicketOrder>
|
||||
*/
|
||||
PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param);
|
||||
|
||||
/**
|
||||
* 关联查询全部
|
||||
*
|
||||
* @param param 查询参数
|
||||
* @return List<GltTicketOrder>
|
||||
*/
|
||||
List<GltTicketOrder> listRel(GltTicketOrderParam param);
|
||||
|
||||
/**
|
||||
* 根据id查询
|
||||
*
|
||||
* @param id
|
||||
* @return GltTicketOrder
|
||||
*/
|
||||
GltTicketOrder getByIdRel(Integer id);
|
||||
|
||||
/**
|
||||
* 下单(事务):校验水票 -> 扣减水票 -> 写核销记录 -> 创建送水订单。
|
||||
*
|
||||
* @param gltTicketOrder 订单请求体
|
||||
* @param userId 当前登录用户ID
|
||||
* @param tenantId 当前租户ID
|
||||
* @return 创建后的订单(含id)
|
||||
*/
|
||||
GltTicketOrder createWithWriteOff(GltTicketOrder gltTicketOrder, Integer userId, Integer tenantId);
|
||||
|
||||
}
|
||||
@@ -39,4 +39,13 @@ public interface GltUserTicketService extends IService<GltUserTicket> {
|
||||
*/
|
||||
GltUserTicket getByIdRel(Integer id);
|
||||
|
||||
/**
|
||||
* 统计指定用户可用水票总数(sum(available_qty))
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID
|
||||
* @return 可用总数(无记录返回0)
|
||||
*/
|
||||
Integer sumAvailableQtyByUserId(Integer userId, Integer tenantId);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.gxwebsoft.glt.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.core.web.PageParam;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||
import com.gxwebsoft.glt.entity.GltUserTicket;
|
||||
import com.gxwebsoft.glt.entity.GltUserTicketLog;
|
||||
import com.gxwebsoft.glt.mapper.GltTicketOrderMapper;
|
||||
import com.gxwebsoft.glt.mapper.GltUserTicketMapper;
|
||||
import com.gxwebsoft.glt.param.GltTicketOrderParam;
|
||||
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
||||
import com.gxwebsoft.glt.service.GltUserTicketLogService;
|
||||
import com.gxwebsoft.glt.service.GltUserTicketService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 送水订单Service实现
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2026-02-05 18:50:20
|
||||
*/
|
||||
@Service
|
||||
public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper, GltTicketOrder> implements GltTicketOrderService {
|
||||
|
||||
public static final int CHANGE_TYPE_WRITE_OFF = 20;
|
||||
|
||||
@Resource
|
||||
private GltUserTicketMapper gltUserTicketMapper;
|
||||
|
||||
@Resource
|
||||
private GltUserTicketService gltUserTicketService;
|
||||
|
||||
@Resource
|
||||
private GltUserTicketLogService gltUserTicketLogService;
|
||||
|
||||
@Override
|
||||
public PageResult<GltTicketOrder> pageRel(GltTicketOrderParam param) {
|
||||
PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>(param);
|
||||
page.setDefaultOrder("sort_number asc, create_time desc");
|
||||
List<GltTicketOrder> list = baseMapper.selectPageRel(page, param);
|
||||
return new PageResult<>(list, page.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GltTicketOrder> listRel(GltTicketOrderParam param) {
|
||||
List<GltTicketOrder> list = baseMapper.selectListRel(param);
|
||||
// 排序
|
||||
PageParam<GltTicketOrder, GltTicketOrderParam> page = new PageParam<>();
|
||||
page.setDefaultOrder("sort_number asc, create_time desc");
|
||||
return page.sortRecords(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GltTicketOrder getByIdRel(Integer id) {
|
||||
GltTicketOrderParam param = new GltTicketOrderParam();
|
||||
param.setId(id);
|
||||
return param.getOne(baseMapper.selectListRel(param));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public GltTicketOrder createWithWriteOff(GltTicketOrder gltTicketOrder, Integer userId, Integer tenantId) {
|
||||
if (gltTicketOrder == null) {
|
||||
throw new BusinessException("订单参数不能为空");
|
||||
}
|
||||
if (userId == null) {
|
||||
throw new BusinessException("请先登录");
|
||||
}
|
||||
Integer userTicketId = gltTicketOrder.getUserTicketId();
|
||||
if (userTicketId == null) {
|
||||
throw new BusinessException("userTicketId不能为空");
|
||||
}
|
||||
int totalNum = gltTicketOrder.getTotalNum() == null ? 0 : gltTicketOrder.getTotalNum();
|
||||
if (totalNum <= 0) {
|
||||
throw new BusinessException("totalNum必须大于0");
|
||||
}
|
||||
if (tenantId == null) {
|
||||
throw new BusinessException("租户信息缺失");
|
||||
}
|
||||
|
||||
// 1) 校验水票归属当前用户 + 正常状态,并锁定记录,避免并发扣减导致日志不准确
|
||||
GltUserTicket userTicket = gltUserTicketMapper.selectByIdForUpdate(userTicketId, userId, tenantId);
|
||||
if (userTicket == null) {
|
||||
throw new BusinessException("水票不存在、已失效或无权限");
|
||||
}
|
||||
int availableQty = userTicket.getAvailableQty() == null ? 0 : userTicket.getAvailableQty();
|
||||
int usedQty = userTicket.getUsedQty() == null ? 0 : userTicket.getUsedQty();
|
||||
if (availableQty < totalNum) {
|
||||
throw new BusinessException("水票数量不足");
|
||||
}
|
||||
|
||||
// 2) 更新 glt_user_ticket: availableQty -= totalNum, usedQty += totalNum
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int availableAfter = availableQty - totalNum;
|
||||
int usedAfter = usedQty + totalNum;
|
||||
userTicket.setAvailableQty(availableAfter);
|
||||
userTicket.setUsedQty(usedAfter);
|
||||
userTicket.setUpdateTime(now);
|
||||
if (!gltUserTicketService.updateById(userTicket)) {
|
||||
throw new BusinessException("扣减水票失败");
|
||||
}
|
||||
|
||||
// 4) 插入 glt_ticket_order(storeId/addressId/totalNum/buyerRemarks…)
|
||||
gltTicketOrder.setUserId(userId);
|
||||
// 订单基础字段由后端兜底,避免前端误传/恶意传参
|
||||
gltTicketOrder.setStatus(0);
|
||||
gltTicketOrder.setDeleted(0);
|
||||
gltTicketOrder.setTenantId(tenantId);
|
||||
if (gltTicketOrder.getSortNumber() == null) {
|
||||
gltTicketOrder.setSortNumber(0);
|
||||
}
|
||||
if (gltTicketOrder.getCreateTime() == null) {
|
||||
gltTicketOrder.setCreateTime(now);
|
||||
}
|
||||
gltTicketOrder.setUpdateTime(now);
|
||||
if (!this.save(gltTicketOrder)) {
|
||||
throw new BusinessException("创建订单失败");
|
||||
}
|
||||
|
||||
// 3) 插入 glt_user_ticket_log(核销记录)
|
||||
GltUserTicketLog log = new GltUserTicketLog();
|
||||
log.setUserTicketId(userTicketId);
|
||||
log.setChangeType(CHANGE_TYPE_WRITE_OFF);
|
||||
log.setChangeAvailable(-totalNum);
|
||||
log.setChangeFrozen(0);
|
||||
log.setChangeUsed(totalNum);
|
||||
log.setAvailableAfter(availableAfter);
|
||||
log.setFrozenAfter(userTicket.getFrozenQty() == null ? 0 : userTicket.getFrozenQty());
|
||||
log.setUsedAfter(usedAfter);
|
||||
log.setOrderId(gltTicketOrder.getId());
|
||||
log.setOrderNo(gltTicketOrder.getId() == null ? null : String.valueOf(gltTicketOrder.getId()));
|
||||
log.setUserId(userId);
|
||||
log.setSortNumber(0);
|
||||
String comments = gltTicketOrder.getComments();
|
||||
if (StrUtil.isBlank(comments)) {
|
||||
comments = gltTicketOrder.getBuyerRemarks();
|
||||
}
|
||||
if (StrUtil.isBlank(comments)) {
|
||||
comments = "水票下单核销";
|
||||
}
|
||||
log.setComments(comments);
|
||||
log.setStatus(0);
|
||||
log.setDeleted(0);
|
||||
log.setTenantId(tenantId);
|
||||
log.setCreateTime(now);
|
||||
log.setUpdateTime(now);
|
||||
if (!gltUserTicketLogService.save(log)) {
|
||||
throw new BusinessException("写入核销记录失败");
|
||||
}
|
||||
|
||||
return gltTicketOrder;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -44,4 +44,10 @@ public class GltUserTicketServiceImpl extends ServiceImpl<GltUserTicketMapper, G
|
||||
return param.getOne(baseMapper.selectListRel(param));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer sumAvailableQtyByUserId(Integer userId, Integer tenantId) {
|
||||
Integer availableQty = baseMapper.sumAvailableQtyByUserId(userId, tenantId);
|
||||
return availableQty == null ? 0 : availableQty;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.gxwebsoft.shop.task;
|
||||
package com.gxwebsoft.glt.task;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
@@ -6,6 +6,7 @@ import com.gxwebsoft.common.core.annotation.IgnoreTenant;
|
||||
import com.gxwebsoft.shop.entity.ShopDealerCapital;
|
||||
import com.gxwebsoft.shop.entity.ShopDealerOrder;
|
||||
import com.gxwebsoft.shop.entity.ShopDealerReferee;
|
||||
import com.gxwebsoft.shop.entity.ShopDealerSetting;
|
||||
import com.gxwebsoft.shop.entity.ShopDealerUser;
|
||||
import com.gxwebsoft.shop.entity.ShopGoods;
|
||||
import com.gxwebsoft.shop.entity.ShopOrder;
|
||||
@@ -15,11 +16,13 @@ import com.gxwebsoft.common.system.mapper.UserMapper;
|
||||
import com.gxwebsoft.shop.service.ShopDealerCapitalService;
|
||||
import com.gxwebsoft.shop.service.ShopDealerOrderService;
|
||||
import com.gxwebsoft.shop.service.ShopDealerRefereeService;
|
||||
import com.gxwebsoft.shop.service.ShopDealerSettingService;
|
||||
import com.gxwebsoft.shop.service.ShopDealerUserService;
|
||||
import com.gxwebsoft.shop.service.ShopGoodsService;
|
||||
import com.gxwebsoft.shop.service.ShopOrderService;
|
||||
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
|
||||
import com.gxwebsoft.shop.util.UpstreamUserFinder;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -48,6 +51,7 @@ public class DealerOrderSettlement10584Task {
|
||||
private static final BigDecimal RATE_0_03 = new BigDecimal("0.03");
|
||||
private static final BigDecimal RATE_0_02 = new BigDecimal("0.02");
|
||||
private static final BigDecimal RATE_0_01 = new BigDecimal("0.01");
|
||||
private static final BigDecimal TOTAL_DEALER_DIVIDEND_RATE = RATE_0_01;
|
||||
|
||||
private static final int MAX_ORDERS_PER_RUN = 50;
|
||||
private static final int MAX_REFEREE_CHAIN_DEPTH = 20;
|
||||
@@ -71,6 +75,9 @@ public class DealerOrderSettlement10584Task {
|
||||
@Resource
|
||||
private ShopDealerOrderService shopDealerOrderService;
|
||||
|
||||
@Resource
|
||||
private ShopDealerSettingService shopDealerSettingService;
|
||||
|
||||
@Resource
|
||||
private ShopGoodsService shopGoodsService;
|
||||
|
||||
@@ -95,6 +102,12 @@ public class DealerOrderSettlement10584Task {
|
||||
// Per-run caches to reduce DB chatter across orders.
|
||||
Map<Integer, Integer> level1ParentCache = new HashMap<>();
|
||||
Map<Integer, Boolean> shopRoleCache = new HashMap<>();
|
||||
DealerBasicSetting dealerBasicSetting = findDealerBasicSetting();
|
||||
ShopDealerUser totalDealerUser = findTotalDealerUser();
|
||||
if (totalDealerUser == null || totalDealerUser.getUserId() == null) {
|
||||
log.warn("未找到总经销商账号,订单仍可结算但不会发放总经销商分润 - tenantId={}", TENANT_ID);
|
||||
}
|
||||
log.debug("租户{}分销设置 - level={}", TENANT_ID, dealerBasicSetting.level);
|
||||
|
||||
log.info("租户{}待结算订单数: {}, orderNos(sample)={}",
|
||||
TENANT_ID,
|
||||
@@ -108,7 +121,7 @@ public class DealerOrderSettlement10584Task {
|
||||
if (!claimOrderToSettle(order.getOrderId())) {
|
||||
return;
|
||||
}
|
||||
settleOneOrder(order, level1ParentCache, shopRoleCache);
|
||||
settleOneOrder(order, level1ParentCache, shopRoleCache, totalDealerUser, dealerBasicSetting.level);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("订单结算失败,将回滚本订单并在下次任务重试 - orderId={}, orderNo={}", order.getOrderId(), order.getOrderNo(), e);
|
||||
@@ -141,7 +154,13 @@ public class DealerOrderSettlement10584Task {
|
||||
);
|
||||
}
|
||||
|
||||
private void settleOneOrder(ShopOrder order, Map<Integer, Integer> level1ParentCache, Map<Integer, Boolean> shopRoleCache) {
|
||||
private void settleOneOrder(
|
||||
ShopOrder order,
|
||||
Map<Integer, Integer> level1ParentCache,
|
||||
Map<Integer, Boolean> shopRoleCache,
|
||||
ShopDealerUser totalDealerUser,
|
||||
int dealerLevel
|
||||
) {
|
||||
if (order.getUserId() == null || order.getOrderNo() == null) {
|
||||
throw new IllegalStateException("订单关键信息缺失,无法结算 - orderId=" + order.getOrderId());
|
||||
}
|
||||
@@ -178,29 +197,52 @@ public class DealerOrderSettlement10584Task {
|
||||
commissionConfig.storeSimpleValue);
|
||||
|
||||
// 1) 直推/间推(shop_dealer_referee)
|
||||
DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount, goodsQty, commissionConfig);
|
||||
DealerRefereeCommission dealerRefereeCommission = settleDealerRefereeCommission(order, baseAmount, goodsQty, commissionConfig, dealerLevel);
|
||||
|
||||
// 2) 门店分红上级:从下单用户开始逐级向上找,命中 ShopDealerUser.type=1 的最近两级(直推门店/间推门店)。
|
||||
ShopRoleCommission shopRoleCommission = settleShopRoleRefereeCommission(order, baseAmount, goodsQty, commissionConfig, level1ParentCache, shopRoleCache);
|
||||
|
||||
// 3) 写入分销订单记录(用于排查/统计;详细分佣以 ShopDealerCapital 为准)
|
||||
createDealerOrderRecord(order, baseAmount, dealerRefereeCommission, shopRoleCommission);
|
||||
// 3) 总经销商分润:固定比率,每个订单都分。
|
||||
TotalDealerCommission totalDealerCommission = settleTotalDealerCommission(order, baseAmount, goodsQty, totalDealerUser);
|
||||
|
||||
// 4) 写入分销订单记录(用于排查/统计;详细分佣以 ShopDealerCapital 为准)
|
||||
createDealerOrderRecord(order, baseAmount, dealerRefereeCommission, shopRoleCommission, totalDealerCommission);
|
||||
|
||||
log.info("订单结算完成 - orderId={}, orderNo={}, baseAmount={}", order.getOrderId(), order.getOrderNo(), baseAmount);
|
||||
}
|
||||
|
||||
private DealerRefereeCommission settleDealerRefereeCommission(ShopOrder order, BigDecimal baseAmount, int goodsQty, CommissionConfig commissionConfig) {
|
||||
private DealerRefereeCommission settleDealerRefereeCommission(
|
||||
ShopOrder order,
|
||||
BigDecimal baseAmount,
|
||||
int goodsQty,
|
||||
CommissionConfig commissionConfig,
|
||||
int dealerLevel
|
||||
) {
|
||||
// 兼容两种数据形态:
|
||||
// 1) 同一 userId 下有 level=1/2 的多级关系(直接按 level 取);
|
||||
// 1) 同一 userId 下有 level=1/2/3 的多级关系(直接按 level 取);
|
||||
// 2) 仅维护 level=1(用“查两次”回退获取上级)。
|
||||
Integer directDealerId = getDealerRefereeId(order.getUserId(), 1);
|
||||
Integer simpleDealerId = getDealerRefereeId(order.getUserId(), 2);
|
||||
if (simpleDealerId == null && directDealerId != null) {
|
||||
simpleDealerId = getDealerRefereeId(directDealerId, 1);
|
||||
//
|
||||
// 严格按“分销设置 level”决定发放到第几级,避免 level=2 时仍触发第3级发放逻辑。
|
||||
int normalizedLevel = normalizeDealerLevel(dealerLevel);
|
||||
|
||||
Integer directDealerId = null;
|
||||
Integer simpleDealerId = null;
|
||||
Integer thirdDealerId = null;
|
||||
|
||||
if (normalizedLevel >= 1) {
|
||||
directDealerId = getDealerRefereeId(order.getUserId(), 1);
|
||||
}
|
||||
Integer thirdDealerId = getDealerRefereeId(order.getUserId(), 3);
|
||||
if (thirdDealerId == null && simpleDealerId != null) {
|
||||
thirdDealerId = getDealerRefereeId(simpleDealerId, 1);
|
||||
if (normalizedLevel >= 2) {
|
||||
simpleDealerId = getDealerRefereeId(order.getUserId(), 2);
|
||||
if (simpleDealerId == null && directDealerId != null) {
|
||||
simpleDealerId = getDealerRefereeId(directDealerId, 1);
|
||||
}
|
||||
}
|
||||
if (normalizedLevel >= 3) {
|
||||
thirdDealerId = getDealerRefereeId(order.getUserId(), 3);
|
||||
if (thirdDealerId == null && simpleDealerId != null) {
|
||||
thirdDealerId = getDealerRefereeId(simpleDealerId, 1);
|
||||
}
|
||||
}
|
||||
|
||||
BigDecimal directMoney =
|
||||
@@ -215,27 +257,33 @@ public class DealerOrderSettlement10584Task {
|
||||
order.getOrderNo(), order.getUserId(), directDealerId, directMoney, simpleDealerId, simpleMoney, thirdDealerId, thirdMoney);
|
||||
|
||||
// 直推:对方=买家;推荐奖(5%):对方=直推分销商(便于在资金明细中看出“来自哪个下级分销商/团队订单”)
|
||||
creditDealerCommission(
|
||||
directDealerId,
|
||||
directMoney,
|
||||
order,
|
||||
order.getUserId(),
|
||||
buildCommissionComment("直推佣金", commissionConfig.commissionType, commissionConfig.dealerDirectValue, goodsQty)
|
||||
);
|
||||
creditDealerCommission(
|
||||
simpleDealerId,
|
||||
simpleMoney,
|
||||
order,
|
||||
directDealerId,
|
||||
buildCommissionComment("推荐奖", commissionConfig.commissionType, commissionConfig.dealerSimpleValue, goodsQty)
|
||||
);
|
||||
creditDealerCommission(
|
||||
thirdDealerId,
|
||||
thirdMoney,
|
||||
order,
|
||||
simpleDealerId,
|
||||
buildCommissionComment("第3级佣金", commissionConfig.commissionType, commissionConfig.dealerThirdValue, goodsQty)
|
||||
);
|
||||
if (normalizedLevel >= 1) {
|
||||
creditDealerCommission(
|
||||
directDealerId,
|
||||
directMoney,
|
||||
order,
|
||||
order.getUserId(),
|
||||
buildCommissionComment("直推佣金", commissionConfig.commissionType, commissionConfig.dealerDirectValue, goodsQty)
|
||||
);
|
||||
}
|
||||
if (normalizedLevel >= 2) {
|
||||
creditDealerCommission(
|
||||
simpleDealerId,
|
||||
simpleMoney,
|
||||
order,
|
||||
directDealerId,
|
||||
buildCommissionComment("推荐奖", commissionConfig.commissionType, commissionConfig.dealerSimpleValue, goodsQty)
|
||||
);
|
||||
}
|
||||
if (normalizedLevel >= 3) {
|
||||
creditDealerCommission(
|
||||
thirdDealerId,
|
||||
thirdMoney,
|
||||
order,
|
||||
simpleDealerId,
|
||||
buildCommissionComment("分润收入", commissionConfig.commissionType, commissionConfig.dealerThirdValue, goodsQty)
|
||||
);
|
||||
}
|
||||
|
||||
return new DealerRefereeCommission(directDealerId, directMoney, simpleDealerId, simpleMoney, thirdDealerId, thirdMoney);
|
||||
}
|
||||
@@ -323,6 +371,83 @@ public class DealerOrderSettlement10584Task {
|
||||
return new ShopRoleCommission(shopRoleReferees.get(0), storeDirectMoney, shopRoleReferees.get(1), storeSimpleMoney);
|
||||
}
|
||||
|
||||
private TotalDealerCommission settleTotalDealerCommission(
|
||||
ShopOrder order,
|
||||
BigDecimal baseAmount,
|
||||
int goodsQty,
|
||||
ShopDealerUser totalDealerUser
|
||||
) {
|
||||
if (totalDealerUser == null || totalDealerUser.getUserId() == null) {
|
||||
return TotalDealerCommission.empty();
|
||||
}
|
||||
BigDecimal rate = safePositive(totalDealerUser.getRate());
|
||||
if (rate.signum() <= 0) {
|
||||
rate = TOTAL_DEALER_DIVIDEND_RATE;
|
||||
}
|
||||
BigDecimal money = calcMoneyByCommissionType(baseAmount, rate, goodsQty, DIVIDEND_SCALE, 20);
|
||||
log.info("总经销商分润发放 - orderNo={}, totalDealerUserId={}, rate={}, money={}",
|
||||
order.getOrderNo(), totalDealerUser.getUserId(), rate, money);
|
||||
creditDealerCommission(
|
||||
totalDealerUser.getUserId(),
|
||||
money,
|
||||
order,
|
||||
order.getUserId(),
|
||||
buildCommissionComment("总经销商分润", 20, rate, goodsQty)
|
||||
);
|
||||
return new TotalDealerCommission(totalDealerUser.getUserId(), money);
|
||||
}
|
||||
|
||||
private ShopDealerUser findTotalDealerUser() {
|
||||
return shopDealerUserService.getOne(
|
||||
new LambdaQueryWrapper<ShopDealerUser>()
|
||||
.eq(ShopDealerUser::getTenantId, TENANT_ID)
|
||||
.eq(ShopDealerUser::getType, 2)
|
||||
.and(w -> w.eq(ShopDealerUser::getIsDelete, 0).or().isNull(ShopDealerUser::getIsDelete))
|
||||
.orderByAsc(ShopDealerUser::getId)
|
||||
.last("limit 1")
|
||||
);
|
||||
}
|
||||
|
||||
private DealerBasicSetting findDealerBasicSetting() {
|
||||
int level = 2;
|
||||
ShopDealerSetting setting = shopDealerSettingService.getOne(
|
||||
new LambdaQueryWrapper<ShopDealerSetting>()
|
||||
.eq(ShopDealerSetting::getTenantId, TENANT_ID)
|
||||
.eq(ShopDealerSetting::getKey, "basic")
|
||||
.last("limit 1")
|
||||
);
|
||||
if (setting != null && setting.getValues() != null && !setting.getValues().isBlank()) {
|
||||
try {
|
||||
JSONObject json = JSONObject.parseObject(setting.getValues());
|
||||
Integer levelVal = json.getInteger("level");
|
||||
if (levelVal != null && levelVal > 0) {
|
||||
level = Math.min(levelVal, 3);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("解析分销设置失败,将使用默认等级 - tenantId={}, values={}", TENANT_ID, setting.getValues(), e);
|
||||
}
|
||||
}
|
||||
return new DealerBasicSetting(level);
|
||||
}
|
||||
|
||||
private int normalizeDealerLevel(int dealerLevel) {
|
||||
if (dealerLevel <= 0) {
|
||||
return 2;
|
||||
}
|
||||
return Math.min(dealerLevel, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* shop_dealer_setting(key=basic) 的关键配置(仅取结算任务需要的字段)。
|
||||
*/
|
||||
private static class DealerBasicSetting {
|
||||
private final int level;
|
||||
|
||||
private DealerBasicSetting(int level) {
|
||||
this.level = level;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店分红规则:
|
||||
* - 门店角色为 ShopDealerUser.type=1;
|
||||
@@ -502,7 +627,8 @@ public class DealerOrderSettlement10584Task {
|
||||
ShopOrder order,
|
||||
BigDecimal baseAmount,
|
||||
DealerRefereeCommission dealerRefereeCommission,
|
||||
ShopRoleCommission shopRoleCommission
|
||||
ShopRoleCommission shopRoleCommission,
|
||||
TotalDealerCommission totalDealerCommission
|
||||
) {
|
||||
// 幂等:同一订单只写一条(依赖 order_no + tenant_id 作为业务唯一)
|
||||
ShopDealerOrder existed = shopDealerOrderService.getOne(
|
||||
@@ -580,20 +706,25 @@ public class DealerOrderSettlement10584Task {
|
||||
dealerOrder.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
|
||||
dealerOrder.setTenantId(TENANT_ID);
|
||||
|
||||
dealerOrder.setComments(buildCommissionTraceComment(dealerRefereeCommission, shopRoleCommission));
|
||||
dealerOrder.setComments(buildCommissionTraceComment(dealerRefereeCommission, shopRoleCommission, totalDealerCommission));
|
||||
|
||||
shopDealerOrderService.save(dealerOrder);
|
||||
log.info("写入ShopDealerOrder完成 - orderNo={}, firstUserId={}, secondUserId={}, firstDividendUser={}, secondDividendUser={}",
|
||||
order.getOrderNo(), dealerOrder.getFirstUserId(), dealerOrder.getSecondUserId(), dealerOrder.getFirstDividendUser(), dealerOrder.getSecondDividendUser());
|
||||
}
|
||||
|
||||
private String buildCommissionTraceComment(DealerRefereeCommission dealerRefereeCommission, ShopRoleCommission shopRoleCommission) {
|
||||
private String buildCommissionTraceComment(
|
||||
DealerRefereeCommission dealerRefereeCommission,
|
||||
ShopRoleCommission shopRoleCommission,
|
||||
TotalDealerCommission totalDealerCommission
|
||||
) {
|
||||
// 轻量“过程”留痕,方便排查;详细分佣以 ShopDealerCapital 为准。
|
||||
return "direct=" + dealerRefereeCommission.directDealerId + ":" + dealerRefereeCommission.directMoney
|
||||
+ ",simple=" + dealerRefereeCommission.simpleDealerId + ":" + dealerRefereeCommission.simpleMoney
|
||||
+ ",third=" + dealerRefereeCommission.thirdDealerId + ":" + dealerRefereeCommission.thirdMoney
|
||||
+ ",dividend1=" + shopRoleCommission.storeDirectUserId + ":" + shopRoleCommission.storeDirectMoney
|
||||
+ ",dividend2=" + shopRoleCommission.storeSimpleUserId + ":" + shopRoleCommission.storeSimpleMoney;
|
||||
+ ",dividend2=" + shopRoleCommission.storeSimpleUserId + ":" + shopRoleCommission.storeSimpleMoney
|
||||
+ ",totalDealer=" + totalDealerCommission.userId + ":" + totalDealerCommission.money;
|
||||
}
|
||||
|
||||
private BigDecimal getOrderBaseAmount(ShopOrder order) {
|
||||
@@ -805,4 +936,18 @@ public class DealerOrderSettlement10584Task {
|
||||
this.storeSimpleMoney = storeSimpleMoney != null ? storeSimpleMoney : BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TotalDealerCommission {
|
||||
private final Integer userId;
|
||||
private final BigDecimal money;
|
||||
|
||||
private static TotalDealerCommission empty() {
|
||||
return new TotalDealerCommission(null, BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
private TotalDealerCommission(Integer userId, BigDecimal money) {
|
||||
this.userId = userId;
|
||||
this.money = money != null ? money : BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.gxwebsoft.common.core.web.BaseController;
|
||||
import com.gxwebsoft.shop.service.ShopDealerSettingService;
|
||||
import com.gxwebsoft.shop.entity.ShopDealerSetting;
|
||||
import com.gxwebsoft.shop.param.ShopDealerSettingParam;
|
||||
import com.gxwebsoft.shop.param.ShopDealerSettingSaveParam;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.common.core.web.PageParam;
|
||||
@@ -59,24 +60,54 @@ public class ShopDealerSettingController extends BaseController {
|
||||
@OperationLog
|
||||
@Operation(summary = "添加分销商设置表")
|
||||
@PostMapping()
|
||||
public ApiResult<?> save(@RequestBody ShopDealerSetting shopDealerSetting) {
|
||||
if (shopDealerSettingService.save(shopDealerSetting)) {
|
||||
return success("添加成功");
|
||||
public ApiResult<?> save(@RequestBody ShopDealerSettingSaveParam param) {
|
||||
ShopDealerSetting shopDealerSetting = buildEntity(param);
|
||||
if (shopDealerSettingService.saveOrUpdateByKey(shopDealerSetting)) {
|
||||
return success("保存成功");
|
||||
}
|
||||
return fail("添加失败");
|
||||
return fail("保存失败");
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('shop:shopDealerSetting:update')")
|
||||
@OperationLog
|
||||
@Operation(summary = "修改分销商设置表")
|
||||
@PutMapping()
|
||||
public ApiResult<?> update(@RequestBody ShopDealerSetting shopDealerSetting) {
|
||||
if (shopDealerSettingService.updateById(shopDealerSetting)) {
|
||||
public ApiResult<?> update(@RequestBody ShopDealerSettingSaveParam param) {
|
||||
ShopDealerSetting shopDealerSetting = buildEntity(param);
|
||||
if (shopDealerSetting.getKey() == null) {
|
||||
return fail("修改失败");
|
||||
}
|
||||
if (shopDealerSettingService.saveOrUpdateByKey(shopDealerSetting)) {
|
||||
return success("修改成功");
|
||||
}
|
||||
return fail("修改失败");
|
||||
}
|
||||
|
||||
private ShopDealerSetting buildEntity(ShopDealerSettingSaveParam param) {
|
||||
ShopDealerSetting shopDealerSetting = new ShopDealerSetting();
|
||||
shopDealerSetting.setKey(param.getKey());
|
||||
shopDealerSetting.setDescribe(param.getDescribe());
|
||||
shopDealerSetting.setValues(param.getValues());
|
||||
Integer tenantId = param.getTenantId();
|
||||
if (tenantId == null) {
|
||||
tenantId = getTenantId();
|
||||
}
|
||||
shopDealerSetting.setTenantId(tenantId);
|
||||
shopDealerSetting.setUpdateTime(normalizeUpdateTime(param.getUpdateTime()));
|
||||
return shopDealerSetting;
|
||||
}
|
||||
|
||||
private Integer normalizeUpdateTime(Long updateTime) {
|
||||
long value = updateTime != null ? updateTime : System.currentTimeMillis();
|
||||
if (value > Integer.MAX_VALUE) {
|
||||
value = value / 1000;
|
||||
}
|
||||
if (value > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
return (int) value;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('shop:shopDealerSetting:remove')")
|
||||
@OperationLog
|
||||
@Operation(summary = "删除分销商设置表")
|
||||
|
||||
@@ -4,6 +4,7 @@ import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.gxwebsoft.common.core.config.ConfigProperties;
|
||||
import com.gxwebsoft.common.core.config.CertificateProperties;
|
||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||
@@ -95,6 +96,8 @@ public class ShopOrderController extends BaseController {
|
||||
@Resource
|
||||
private ShopOrderDeliveryService shopOrderDeliveryService;
|
||||
@Resource
|
||||
private ShopWechatShippingSyncService shopWechatShippingSyncService;
|
||||
@Resource
|
||||
private PaymentService paymentService;
|
||||
|
||||
@Operation(summary = "分页查询订单")
|
||||
@@ -261,139 +264,213 @@ public class ShopOrderController extends BaseController {
|
||||
@Operation(summary = "修改订单")
|
||||
@PutMapping()
|
||||
public ApiResult<?> update(@RequestBody ShopOrder shopOrder) throws Exception {
|
||||
// 1. 验证订单是否可以退款
|
||||
if (shopOrder == null) {
|
||||
return fail("订单不存在");
|
||||
return fail("订单不存在");
|
||||
}
|
||||
// 退款相关操作单独走退款接口,便于做财务权限隔离
|
||||
if (Objects.equals(shopOrder.getOrderStatus(), 6)) {
|
||||
return fail("退款相关操作请使用退款接口: PUT /api/shop/shop-order/refund");
|
||||
}
|
||||
ShopOrder shopOrderNow = shopOrderService.getById(shopOrder.getOrderId());
|
||||
if (shopOrderNow == null) {
|
||||
return fail("订单不存在");
|
||||
}
|
||||
// 申请退款
|
||||
if (Objects.equals(shopOrder.getOrderStatus(), 4)) {
|
||||
shopOrder.setRefundApplyTime(LocalDateTime.now());
|
||||
}
|
||||
// 发货状态从“未发货(10)”变更为“已发货(20)”时,记录发货信息
|
||||
if (Objects.equals(shopOrderNow.getDeliveryStatus(), 10) && Objects.equals(shopOrder.getDeliveryStatus(), 20)) {
|
||||
ShopOrderDelivery shopOrderDelivery = new ShopOrderDelivery();
|
||||
shopOrderDelivery.setOrderId(shopOrder.getOrderId());
|
||||
shopOrderDelivery.setDeliveryMethod(30);
|
||||
shopOrderDelivery.setExpressId(shopOrder.getExpressId());
|
||||
shopOrderDelivery.setSendName(shopOrder.getSendName());
|
||||
shopOrderDelivery.setSendPhone(shopOrder.getSendPhone());
|
||||
shopOrderDelivery.setSendAddress(shopOrder.getSendAddress());
|
||||
shopOrderDeliveryService.save(shopOrderDelivery);
|
||||
// 1) 无需物流/自提:不走快递100下单,直接置为已发货并同步到微信后台
|
||||
if (shopOrder.getExpressId() == null || shopOrder.getExpressId() == 0) {
|
||||
ShopOrderDelivery shopOrderDelivery = new ShopOrderDelivery();
|
||||
shopOrderDelivery.setOrderId(shopOrder.getOrderId());
|
||||
shopOrderDelivery.setDeliveryMethod(20);
|
||||
shopOrderDelivery.setSendName(shopOrder.getSendName());
|
||||
shopOrderDelivery.setSendPhone(shopOrder.getSendPhone());
|
||||
shopOrderDelivery.setSendAddress(shopOrder.getSendAddress());
|
||||
shopOrderDeliveryService.save(shopOrderDelivery);
|
||||
|
||||
shopOrderDeliveryService.setExpress(getLoginUser(), shopOrderDelivery, shopOrder);
|
||||
ShopOrder patch = new ShopOrder();
|
||||
patch.setOrderId(shopOrder.getOrderId());
|
||||
patch.setDeliveryStatus(20);
|
||||
patch.setDeliveryTime(LocalDateTime.now());
|
||||
shopOrderService.updateById(patch);
|
||||
|
||||
// 同步到微信后台(发货信息录入)
|
||||
ShopOrder syncOrder = shopOrderNow;
|
||||
syncOrder.setDeliveryStatus(20);
|
||||
syncOrder.setDeliveryTime(patch.getDeliveryTime());
|
||||
try {
|
||||
shopWechatShippingSyncService.uploadNoLogisticsShippingInfo(syncOrder);
|
||||
} catch (Exception e) {
|
||||
logger.warn("同步微信发货信息失败(无需物流,不影响发货成功) - orderId={}", shopOrder.getOrderId(), e);
|
||||
}
|
||||
} else {
|
||||
// 2) 实物快递:创建发货单并走快递100电子面单,发货成功后同步微信后台
|
||||
ShopOrderDelivery shopOrderDelivery = new ShopOrderDelivery();
|
||||
shopOrderDelivery.setOrderId(shopOrder.getOrderId());
|
||||
shopOrderDelivery.setDeliveryMethod(30);
|
||||
shopOrderDelivery.setExpressId(shopOrder.getExpressId());
|
||||
shopOrderDelivery.setSendName(shopOrder.getSendName());
|
||||
shopOrderDelivery.setSendPhone(shopOrder.getSendPhone());
|
||||
shopOrderDelivery.setSendAddress(shopOrder.getSendAddress());
|
||||
shopOrderDeliveryService.save(shopOrderDelivery);
|
||||
|
||||
// 需要用订单的持久化字段(例如 addressId/tenantId/userId/transactionId),同时补齐临时的发货地址
|
||||
ShopOrder orderForDelivery = shopOrderNow;
|
||||
orderForDelivery.setSendAddress(shopOrder.getSendAddress());
|
||||
shopOrderDeliveryService.setExpress(getLoginUser(), shopOrderDelivery, orderForDelivery);
|
||||
}
|
||||
// 退款操作
|
||||
if (Objects.equals(shopOrder.getOrderStatus(), 6)) {
|
||||
// 当订单状态更改为6(已退款)时,执行退款操作
|
||||
try {
|
||||
|
||||
// 检查订单是否已支付
|
||||
if (!Boolean.TRUE.equals(shopOrderNow.getPayStatus())) {
|
||||
return fail("订单未支付,无法退款");
|
||||
}
|
||||
|
||||
// 检查是否已经退款过了
|
||||
if (StrUtil.isNotBlank(shopOrderNow.getRefundOrder())) {
|
||||
logger.warn("订单已经退款过,订单号: {}, 退款单号: {}", shopOrderNow.getOrderNo(), shopOrderNow.getRefundOrder());
|
||||
return fail("订单已退款,请勿重复操作");
|
||||
}
|
||||
|
||||
// 2. 生成退款单号
|
||||
String refundNo = "RF" + IdUtil.getSnowflakeNextId();
|
||||
|
||||
// 3. 确定退款金额(默认全额退款)
|
||||
BigDecimal refundAmount = shopOrder.getRefundMoney();
|
||||
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
// 如果没有指定退款金额,使用订单实付金额
|
||||
refundAmount = shopOrderNow.getTotalPrice();
|
||||
}
|
||||
|
||||
// 验证退款金额不能大于订单金额
|
||||
if (refundAmount.compareTo(shopOrderNow.getTotalPrice()) > 0) {
|
||||
return fail("退款金额不能大于订单金额");
|
||||
}
|
||||
|
||||
// 4. 确定支付类型(默认为微信Native支付)
|
||||
PaymentType paymentType = PaymentType.WECHAT_NATIVE;
|
||||
if (shopOrderNow.getPayType() != null) {
|
||||
// 根据订单的支付类型确定
|
||||
// 支付方式:0余额支付,1微信支付,2支付宝支付,3银联支付,4现金支付,5POS机支付,6免费,7积分支付
|
||||
paymentType = PaymentType.getByCode(shopOrderNow.getPayType());
|
||||
|
||||
// 如果是微信支付,需要根据微信支付子类型确定具体的支付方式
|
||||
if (paymentType == PaymentType.WECHAT) {
|
||||
// 目前统一使用WECHAT_NATIVE进行退款
|
||||
paymentType = PaymentType.WECHAT_NATIVE;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 调用统一支付服务的退款接口
|
||||
logger.info("开始处理订单退款 - 订单号: {}, 退款单号: {}, 退款金额: {}, 支付方式: {}",
|
||||
shopOrderNow.getOrderNo(), refundNo, refundAmount, paymentType);
|
||||
|
||||
PaymentResponse refundResponse = paymentService.refund(
|
||||
shopOrderNow.getOrderNo(), // 原订单号
|
||||
refundNo, // 退款单号
|
||||
paymentType, // 支付方式
|
||||
shopOrderNow.getTotalPrice(), // 订单总金额
|
||||
refundAmount, // 退款金额
|
||||
shopOrder.getRefundReason() != null ? shopOrder.getRefundReason() : "用户申请退款", // 退款原因
|
||||
shopOrderNow.getTenantId() // 租户ID
|
||||
);
|
||||
|
||||
// 6. 处理退款结果
|
||||
if (refundResponse.getSuccess()) {
|
||||
// 退款成功,更新订单信息
|
||||
shopOrder.setRefundOrder(refundNo);
|
||||
shopOrder.setRefundMoney(refundAmount);
|
||||
shopOrder.setRefundTime(LocalDateTime.now());
|
||||
shopOrder.setOrderStatus(6); // 退款成功
|
||||
|
||||
// 根据退款状态决定订单状态
|
||||
// 如果微信返回退款处理中,则设置订单状态为5(退款处理中)
|
||||
// 如果微信返回退款成功,则保持状态为6(退款成功)
|
||||
// if (refundResponse.getPaymentStatus() != null) {
|
||||
// switch (refundResponse.getPaymentStatus()) {
|
||||
// case REFUNDING:
|
||||
// shopOrder.setOrderStatus(5); // 退款处理中
|
||||
// logger.info("订单退款处理中,订单号: {}, 退款单号: {}", shopOrderNow.getOrderNo(), refundNo);
|
||||
// break;
|
||||
// case REFUNDED:
|
||||
// shopOrder.setOrderStatus(6); // 退款成功
|
||||
// logger.info("订单退款成功,订单号: {}, 退款单号: {}", shopOrderNow.getOrderNo(), refundNo);
|
||||
// break;
|
||||
// case REFUND_FAILED:
|
||||
// logger.error("订单退款失败,订单号: {}, 退款单号: {}", shopOrderNow.getOrderNo(), refundNo);
|
||||
// return fail("退款失败,请联系管理员");
|
||||
// default:
|
||||
// shopOrder.setOrderStatus(5); // 默认为退款处理中
|
||||
// }
|
||||
// }
|
||||
|
||||
logger.info("订单退款请求成功 - 订单号: {}, 退款单号: {}, 微信退款单号: {}",
|
||||
shopOrderNow.getOrderNo(), refundNo, refundResponse.getTransactionId());
|
||||
} else {
|
||||
// 退款失败
|
||||
logger.error("订单退款失败 - 订单号: {}, 错误: {}", shopOrderNow.getOrderNo(), refundResponse.getErrorMessage());
|
||||
return fail("退款失败: " + refundResponse.getErrorMessage());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理订单退款异常 - 订单号: {}, 错误: {}", shopOrderNow.getOrderNo(), e.getMessage(), e);
|
||||
return fail("退款处理异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
if (shopOrderService.updateById(shopOrder)) {
|
||||
// 如果订单上带了快递单号(常见于后台手工修正/补录),同步到发货单表,避免发货单还是旧单号
|
||||
if (StrUtil.isNotBlank(shopOrder.getExpressNo()) && shopOrder.getOrderId() != null) {
|
||||
try {
|
||||
List<ShopOrderDelivery> deliveryList = shopOrderDeliveryService.list(
|
||||
new LambdaQueryWrapper<ShopOrderDelivery>()
|
||||
.eq(ShopOrderDelivery::getOrderId, shopOrder.getOrderId())
|
||||
);
|
||||
if (deliveryList == null || deliveryList.isEmpty()) {
|
||||
ShopOrderDelivery delivery = new ShopOrderDelivery();
|
||||
delivery.setOrderId(shopOrder.getOrderId());
|
||||
delivery.setTenantId(ObjectUtil.defaultIfNull(shopOrder.getTenantId(), shopOrderNow.getTenantId()));
|
||||
delivery.setExpressId(ObjectUtil.defaultIfNull(shopOrder.getExpressId(), shopOrderNow.getExpressId()));
|
||||
delivery.setExpressNo(shopOrder.getExpressNo());
|
||||
// 10手动录入 / 20无需物流
|
||||
if (delivery.getExpressId() == null || delivery.getExpressId() == 0) {
|
||||
delivery.setDeliveryMethod(20);
|
||||
} else {
|
||||
delivery.setDeliveryMethod(10);
|
||||
}
|
||||
delivery.setSendName(ObjectUtil.defaultIfNull(shopOrder.getSendName(), shopOrderNow.getSendName()));
|
||||
delivery.setSendPhone(ObjectUtil.defaultIfNull(shopOrder.getSendPhone(), shopOrderNow.getSendPhone()));
|
||||
delivery.setSendAddress(ObjectUtil.defaultIfNull(shopOrder.getSendAddress(), shopOrderNow.getSendAddress()));
|
||||
shopOrderDeliveryService.save(delivery);
|
||||
} else {
|
||||
for (ShopOrderDelivery d : deliveryList) {
|
||||
if (d == null || d.getDeliveryId() == null) {
|
||||
continue;
|
||||
}
|
||||
ShopOrderDelivery patch = new ShopOrderDelivery();
|
||||
patch.setDeliveryId(d.getDeliveryId());
|
||||
patch.setExpressNo(shopOrder.getExpressNo());
|
||||
shopOrderDeliveryService.updateById(patch);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("同步更新发货单运单号失败 - orderId={}, expressNo={}",
|
||||
shopOrder.getOrderId(), shopOrder.getExpressNo(), e);
|
||||
}
|
||||
}
|
||||
return success("修改成功");
|
||||
}
|
||||
return fail("修改失败");
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('shop:shopOrder:refund')")
|
||||
@Operation(summary = "订单退款操作(申请退款/同意退款)", description = "orderStatus=4 申请退款;orderStatus=6 同意退款并发起原路退款")
|
||||
@PutMapping("/refund")
|
||||
public ApiResult<?> refund(@RequestBody ShopOrder req) {
|
||||
if (req == null || req.getOrderId() == null || req.getOrderStatus() == null) {
|
||||
return fail("orderId 和 orderStatus 不能为空");
|
||||
}
|
||||
if (!Objects.equals(req.getOrderStatus(), 4) && !Objects.equals(req.getOrderStatus(), 6)) {
|
||||
return fail("orderStatus 仅支持 4(申请退款) 或 6(同意退款)");
|
||||
}
|
||||
|
||||
ShopOrder current = shopOrderService.getById(req.getOrderId());
|
||||
if (current == null) {
|
||||
return fail("订单不存在");
|
||||
}
|
||||
|
||||
// 申请退款:只记录申请时间/原因/金额(如有)
|
||||
if (Objects.equals(req.getOrderStatus(), 4)) {
|
||||
ShopOrder patch = new ShopOrder();
|
||||
patch.setOrderId(req.getOrderId());
|
||||
patch.setOrderStatus(4);
|
||||
patch.setRefundApplyTime(LocalDateTime.now());
|
||||
if (StrUtil.isNotBlank(req.getRefundReason())) {
|
||||
patch.setRefundReason(req.getRefundReason());
|
||||
}
|
||||
if (req.getRefundMoney() != null) {
|
||||
patch.setRefundMoney(req.getRefundMoney());
|
||||
}
|
||||
if (shopOrderService.updateById(patch)) {
|
||||
return success("申请退款成功");
|
||||
}
|
||||
return fail("申请退款失败");
|
||||
}
|
||||
|
||||
// 同意退款:发起原路退款并更新退款信息
|
||||
try {
|
||||
if (!Boolean.TRUE.equals(current.getPayStatus())) {
|
||||
return fail("订单未支付,无法退款");
|
||||
}
|
||||
if (StrUtil.isNotBlank(current.getRefundOrder())) {
|
||||
logger.warn("订单已经退款过,订单号: {}, 退款单号: {}", current.getOrderNo(), current.getRefundOrder());
|
||||
return fail("订单已退款,请勿重复操作");
|
||||
}
|
||||
|
||||
String refundNo = "RF" + IdUtil.getSnowflakeNextId();
|
||||
|
||||
BigDecimal refundAmount = req.getRefundMoney();
|
||||
if (refundAmount == null || refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
refundAmount = current.getTotalPrice();
|
||||
}
|
||||
if (refundAmount.compareTo(current.getTotalPrice()) > 0) {
|
||||
return fail("退款金额不能大于订单金额");
|
||||
}
|
||||
|
||||
PaymentType paymentType = PaymentType.WECHAT_NATIVE;
|
||||
if (current.getPayType() != null) {
|
||||
// 支付方式:0余额支付,1微信支付,2支付宝支付,3银联支付,4现金支付,5POS机支付,6免费,7积分支付
|
||||
paymentType = PaymentType.getByCode(current.getPayType());
|
||||
if (paymentType == PaymentType.WECHAT) {
|
||||
paymentType = PaymentType.WECHAT_NATIVE;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("开始处理订单退款 - 订单号: {}, 退款单号: {}, 退款金额: {}, 支付方式: {}",
|
||||
current.getOrderNo(), refundNo, refundAmount, paymentType);
|
||||
|
||||
PaymentResponse refundResponse = paymentService.refund(
|
||||
current.getOrderNo(),
|
||||
refundNo,
|
||||
paymentType,
|
||||
current.getTotalPrice(),
|
||||
refundAmount,
|
||||
StrUtil.isNotBlank(req.getRefundReason()) ? req.getRefundReason() : "用户申请退款",
|
||||
current.getTenantId()
|
||||
);
|
||||
|
||||
if (!Boolean.TRUE.equals(refundResponse.getSuccess())) {
|
||||
logger.error("订单退款失败 - 订单号: {}, 错误: {}", current.getOrderNo(), refundResponse.getErrorMessage());
|
||||
return fail("退款失败: " + refundResponse.getErrorMessage());
|
||||
}
|
||||
|
||||
ShopOrder patch = new ShopOrder();
|
||||
patch.setOrderId(req.getOrderId());
|
||||
patch.setRefundOrder(refundNo);
|
||||
patch.setRefundMoney(refundAmount);
|
||||
patch.setRefundTime(LocalDateTime.now());
|
||||
patch.setOrderStatus(6);
|
||||
if (StrUtil.isNotBlank(req.getRefundReason())) {
|
||||
patch.setRefundReason(req.getRefundReason());
|
||||
}
|
||||
|
||||
if (!shopOrderService.updateById(patch)) {
|
||||
logger.error("退款已成功但订单更新失败 - orderId={}, orderNo={}, refundNo={}", current.getOrderId(), current.getOrderNo(), refundNo);
|
||||
return fail("退款成功,但订单状态更新失败,请联系管理员");
|
||||
}
|
||||
|
||||
logger.info("订单退款请求成功 - 订单号: {}, 退款单号: {}, 微信退款单号: {}",
|
||||
current.getOrderNo(), refundNo, refundResponse.getTransactionId());
|
||||
return success("退款成功");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("处理订单退款异常 - 订单号: {}, 错误: {}", current.getOrderNo(), e.getMessage(), e);
|
||||
return fail("退款处理异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "删除订单")
|
||||
@DeleteMapping("/{id}")
|
||||
public ApiResult<?> remove(@PathVariable("id") Integer id) {
|
||||
@@ -415,6 +492,13 @@ public class ShopOrderController extends BaseController {
|
||||
@Operation(summary = "批量修改订单")
|
||||
@PutMapping("/batch")
|
||||
public ApiResult<?> removeBatch(@RequestBody BatchParam<ShopOrder> batchParam) {
|
||||
if (batchParam != null && batchParam.getData() != null) {
|
||||
Integer status = batchParam.getData().getOrderStatus();
|
||||
// 退款相关操作单独走退款接口,避免绕过财务权限
|
||||
if (Objects.equals(status, 4) || Objects.equals(status, 6)) {
|
||||
return fail("退款相关操作请使用退款接口: PUT /api/shop/shop-order/refund");
|
||||
}
|
||||
}
|
||||
if (batchParam.update(shopOrderService, "order_id")) {
|
||||
return success("修改成功");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.gxwebsoft.shop.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import java.io.Serializable;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -20,13 +21,15 @@ public class ShopDealerSetting implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "设置项标示")
|
||||
@TableId(value = "key", type = IdType.AUTO)
|
||||
@TableId(value = "`key`", type = IdType.INPUT)
|
||||
private String key;
|
||||
|
||||
@Schema(description = "设置项描述")
|
||||
@TableField(value = "`describe`")
|
||||
private String describe;
|
||||
|
||||
@Schema(description = "设置内容(json格式)")
|
||||
@TableField(value = "`values`")
|
||||
private String values;
|
||||
|
||||
@Schema(description = "商城ID")
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
FROM shop_dealer_setting a
|
||||
<where>
|
||||
<if test="param.key != null">
|
||||
AND a.key = #{param.key}
|
||||
AND a.`key` = #{param.key}
|
||||
</if>
|
||||
<if test="param.describe != null">
|
||||
AND a.describe LIKE CONCAT('%', #{param.describe}, '%')
|
||||
AND a.`describe` LIKE CONCAT('%', #{param.describe}, '%')
|
||||
</if>
|
||||
<if test="param.values != null">
|
||||
AND a.values LIKE CONCAT('%', #{param.values}, '%')
|
||||
AND a.`values` LIKE CONCAT('%', #{param.values}, '%')
|
||||
</if>
|
||||
<if test="param.keywords != null">
|
||||
AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%')
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.gxwebsoft.shop.param;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Shop dealer setting save parameters.
|
||||
*
|
||||
* @author WebSoft
|
||||
* @since 2025-08-11 23:51:41
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@Schema(name = "ShopDealerSettingSaveParam", description = "Shop dealer setting save parameters")
|
||||
public class ShopDealerSettingSaveParam implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "Setting key")
|
||||
private String key;
|
||||
|
||||
@Schema(description = "Setting description")
|
||||
private String describe;
|
||||
|
||||
@Schema(description = "Settings JSON content")
|
||||
private String values;
|
||||
|
||||
@Schema(description = "Tenant ID")
|
||||
private Integer tenantId;
|
||||
|
||||
@Schema(description = "Update time")
|
||||
private Long updateTime;
|
||||
}
|
||||
@@ -39,4 +39,12 @@ public interface ShopDealerSettingService extends IService<ShopDealerSetting> {
|
||||
*/
|
||||
ShopDealerSetting getByIdRel(String key);
|
||||
|
||||
/**
|
||||
* 根据 key + tenantId 保存或更新
|
||||
*
|
||||
* @param setting 设置项
|
||||
* @return boolean
|
||||
*/
|
||||
boolean saveOrUpdateByKey(ShopDealerSetting setting);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.gxwebsoft.shop.service;
|
||||
|
||||
import com.gxwebsoft.shop.entity.ShopExpress;
|
||||
import com.gxwebsoft.shop.entity.ShopOrder;
|
||||
import com.gxwebsoft.shop.entity.ShopOrderDelivery;
|
||||
|
||||
/**
|
||||
* 微信小程序“发货信息管理”同步服务。
|
||||
*
|
||||
* <p>用于将系统内发货/无需物流状态同步到微信小程序后台,避免人工在后台录入。</p>
|
||||
*/
|
||||
public interface ShopWechatShippingSyncService {
|
||||
|
||||
/**
|
||||
* 实物快递发货同步到微信后台(上传运单号/快递公司)。
|
||||
*/
|
||||
boolean uploadExpressShippingInfo(ShopOrder order, ShopOrderDelivery orderDelivery, ShopExpress express);
|
||||
|
||||
/**
|
||||
* 无需物流/自提发货同步到微信后台(上传无需物流)。
|
||||
*/
|
||||
boolean uploadNoLogisticsShippingInfo(ShopOrder order);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import com.gxwebsoft.shop.entity.ShopDealerSetting;
|
||||
import com.gxwebsoft.shop.param.ShopDealerSettingParam;
|
||||
import com.gxwebsoft.common.core.web.PageParam;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -23,7 +25,7 @@ public class ShopDealerSettingServiceImpl extends ServiceImpl<ShopDealerSettingM
|
||||
@Override
|
||||
public PageResult<ShopDealerSetting> pageRel(ShopDealerSettingParam param) {
|
||||
PageParam<ShopDealerSetting, ShopDealerSettingParam> page = new PageParam<>(param);
|
||||
page.setDefaultOrder("create_time desc");
|
||||
page.setDefaultOrder("update_time desc");
|
||||
List<ShopDealerSetting> list = baseMapper.selectPageRel(page, param);
|
||||
return new PageResult<>(list, page.getTotal());
|
||||
}
|
||||
@@ -31,10 +33,14 @@ public class ShopDealerSettingServiceImpl extends ServiceImpl<ShopDealerSettingM
|
||||
@Override
|
||||
public List<ShopDealerSetting> listRel(ShopDealerSettingParam param) {
|
||||
List<ShopDealerSetting> list = baseMapper.selectListRel(param);
|
||||
// 排序
|
||||
PageParam<ShopDealerSetting, ShopDealerSettingParam> page = new PageParam<>();
|
||||
page.setDefaultOrder("create_time desc");
|
||||
return page.sortRecords(list);
|
||||
if (list == null || list.size() < 2) {
|
||||
return list;
|
||||
}
|
||||
list.sort(Comparator.comparing(
|
||||
ShopDealerSetting::getUpdateTime,
|
||||
Comparator.nullsLast(Integer::compareTo)
|
||||
).reversed());
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -44,4 +50,17 @@ public class ShopDealerSettingServiceImpl extends ServiceImpl<ShopDealerSettingM
|
||||
return param.getOne(baseMapper.selectListRel(param));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean saveOrUpdateByKey(ShopDealerSetting setting) {
|
||||
if (setting == null || setting.getKey() == null) {
|
||||
return false;
|
||||
}
|
||||
LambdaQueryWrapper<ShopDealerSetting> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(ShopDealerSetting::getKey, setting.getKey());
|
||||
if (setting.getTenantId() != null) {
|
||||
wrapper.eq(ShopDealerSetting::getTenantId, setting.getTenantId());
|
||||
}
|
||||
return this.saveOrUpdate(setting, wrapper);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.gxwebsoft.common.core.web.PageParam;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.kuaidi100.sdk.pojo.HttpResult;
|
||||
import com.kuaidi100.sdk.request.BOrderReq;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@@ -27,6 +28,7 @@ import java.util.Map;
|
||||
* @since 2025-01-11 10:45:12
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ShopOrderDeliveryServiceImpl extends ServiceImpl<ShopOrderDeliveryMapper, ShopOrderDelivery> implements ShopOrderDeliveryService {
|
||||
@Resource
|
||||
private ShopExpressService expressService;
|
||||
@@ -40,6 +42,8 @@ public class ShopOrderDeliveryServiceImpl extends ServiceImpl<ShopOrderDeliveryM
|
||||
private ShopGoodsService shopGoodsService;
|
||||
@Resource
|
||||
private ShopUserAddressService shopUserAddressService;
|
||||
@Resource
|
||||
private ShopWechatShippingSyncService shopWechatShippingSyncService;
|
||||
|
||||
@Override
|
||||
public PageResult<ShopOrderDelivery> pageRel(ShopOrderDeliveryParam param) {
|
||||
@@ -111,45 +115,12 @@ public class ShopOrderDeliveryServiceImpl extends ServiceImpl<ShopOrderDeliveryM
|
||||
order.setDeliveryTime(LocalDateTime.now());
|
||||
shopOrderService.updateById(order);
|
||||
|
||||
if (order.getPayType().equals(1)) {
|
||||
List<ShopOrderGoods> orderGoodsList = shopOrderGoodsService.getListByOrderId(order.getOrderId());
|
||||
// 上传小程序发货信息
|
||||
// WxMaOrderShippingInfoUploadRequest uploadRequest = new WxMaOrderShippingInfoUploadRequest();
|
||||
// uploadRequest.setLogisticsType(1);
|
||||
// uploadRequest.setDeliveryMode(1);
|
||||
//
|
||||
// OrderKeyBean orderKeyBean = new OrderKeyBean();
|
||||
// orderKeyBean.setOrderNumberType(2);
|
||||
// orderKeyBean.setTransactionId(order.getTransactionId());
|
||||
// uploadRequest.setOrderKey(orderKeyBean);
|
||||
//
|
||||
// List<ShippingListBean> shippingList = new ArrayList<>();
|
||||
// ShippingListBean shippingListBean = new ShippingListBean();
|
||||
// shippingListBean.setTrackingNo((String) bOrderData.get("kuaidinum"));
|
||||
// shippingListBean.setExpressCompany(express.getWxCode());
|
||||
// ContactBean contactBean = new ContactBean();
|
||||
// contactBean.setReceiverContact(user.getMobile());
|
||||
// shippingListBean.setContact(contactBean);
|
||||
//
|
||||
// ShopGoods shopGoods = shopGoodsService.getById(orderGoodsList.get(0).getGoodsId());
|
||||
//
|
||||
// String itemDesc = shopGoods.getName();
|
||||
// if (orderGoodsList.size() > 1) itemDesc += "等" + orderGoodsList.size() + "件商品";
|
||||
// shippingListBean.setItemDesc(itemDesc);
|
||||
// shippingList.add(shippingListBean);
|
||||
// uploadRequest.setShippingList(shippingList);
|
||||
//
|
||||
// uploadRequest.setUploadTime(new DateTime().toString(DatePattern.UTC_WITH_ZONE_OFFSET_PATTERN));
|
||||
//
|
||||
// PayerBean payerBean = new PayerBean();
|
||||
//
|
||||
// payerBean.setOpenid(user.getOpenid());
|
||||
// uploadRequest.setPayer(payerBean);
|
||||
//
|
||||
// WxMaService wxMaService = weChatController.wxMaService();
|
||||
// WxMaOrderShippingService wxMaOrderShippingService = new WxMaOrderShippingServiceImpl(wxMaService);
|
||||
// WxMaOrderShippingInfoBaseResponse response = wxMaOrderShippingService.upload(uploadRequest);
|
||||
// System.out.println("response" + response);
|
||||
// 同步发货信息到微信小程序后台(发货信息录入),避免人工录入
|
||||
try {
|
||||
shopWechatShippingSyncService.uploadExpressShippingInfo(order, orderDelivery, express);
|
||||
} catch (Exception e) {
|
||||
// 不影响本地发货流程,记录日志即可(可配合定时任务后补偿重试)
|
||||
log.warn("同步微信发货信息失败(不影响发货成功) - orderId={}", order.getOrderId(), e);
|
||||
}
|
||||
return new HashMap<>() {{
|
||||
put("res", true);
|
||||
|
||||
@@ -45,12 +45,19 @@ public class ShopWebsiteServiceImpl implements ShopWebsiteService {
|
||||
String cacheKey = SHOP_INFO_KEY_PREFIX + tenantId;
|
||||
String shopInfo = redisUtil.get(cacheKey);
|
||||
if (StrUtil.isNotBlank(shopInfo)) {
|
||||
log.info("从缓存获取商城信息,租户ID: {}", tenantId);
|
||||
// try {
|
||||
// return JSONUtil.parseObject(shopInfo, ShopVo.class);
|
||||
// } catch (Exception e) {
|
||||
// log.warn("商城缓存解析失败,从数据库重新获取: {}", e.getMessage());
|
||||
// }
|
||||
try {
|
||||
ShopVo cacheVo = JSONUtil.parseObject(shopInfo, ShopVo.class);
|
||||
if (cacheVo != null) {
|
||||
log.info("从缓存获取商城信息,租户ID: {}", tenantId);
|
||||
return cacheVo;
|
||||
}
|
||||
// 兼容历史缓存:JSON "null" 会被解析为 null;此时清理缓存并回源。
|
||||
log.warn("商城信息缓存命中但内容为空(null),清理缓存后回源数据库,租户ID: {}", tenantId);
|
||||
redisUtil.delete(cacheKey);
|
||||
} catch (Exception e) {
|
||||
log.warn("商城缓存解析失败,清理缓存后重新获取: {}", e.getMessage());
|
||||
redisUtil.delete(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
// 直接调用 CMS 服务获取站点信息,然后使用商城专用缓存
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
package com.gxwebsoft.shop.service.impl;
|
||||
|
||||
import cn.binarywang.wx.miniapp.bean.shop.request.shipping.OrderKeyBean;
|
||||
import cn.binarywang.wx.miniapp.bean.shop.request.shipping.PayerBean;
|
||||
import cn.binarywang.wx.miniapp.bean.shop.request.shipping.ShippingListBean;
|
||||
import cn.binarywang.wx.miniapp.bean.shop.request.shipping.WxMaOrderShippingInfoUploadRequest;
|
||||
import cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants;
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.DateTime;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||
import com.gxwebsoft.common.system.entity.Payment;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.common.system.service.UserService;
|
||||
import com.gxwebsoft.common.system.service.WxMiniappAccessTokenService;
|
||||
import com.gxwebsoft.shop.entity.ShopExpress;
|
||||
import com.gxwebsoft.shop.entity.ShopGoods;
|
||||
import com.gxwebsoft.shop.entity.ShopOrder;
|
||||
import com.gxwebsoft.shop.entity.ShopOrderDelivery;
|
||||
import com.gxwebsoft.shop.entity.ShopOrderGoods;
|
||||
import com.gxwebsoft.shop.service.ShopGoodsService;
|
||||
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
|
||||
import com.gxwebsoft.shop.service.ShopWechatShippingSyncService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 同步系统发货信息到微信小程序后台(发货信息录入)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ShopWechatShippingSyncServiceImpl implements ShopWechatShippingSyncService {
|
||||
|
||||
private static final int ORDER_NUMBER_TYPE_OUT_TRADE_NO = 1;
|
||||
private static final int ORDER_NUMBER_TYPE_TRANSACTION_ID = 2;
|
||||
|
||||
// 这两个值在项目原有注释代码中已经使用过(实物快递)。
|
||||
private static final int LOGISTICS_TYPE_PHYSICAL = 1;
|
||||
private static final int DELIVERY_MODE_EXPRESS = 1;
|
||||
|
||||
// 无需物流/自提:微信侧会在“发货信息录入”里变为已发货(具体枚举以微信接口为准)。
|
||||
private static final int LOGISTICS_TYPE_NO_LOGISTICS = 3;
|
||||
private static final int DELIVERY_MODE_NO_LOGISTICS = 3;
|
||||
|
||||
private static final Gson GSON = new Gson();
|
||||
|
||||
@Resource
|
||||
private WxMiniappAccessTokenService wxMiniappAccessTokenService;
|
||||
@Resource
|
||||
private UserService userService;
|
||||
@Resource
|
||||
private ShopOrderGoodsService shopOrderGoodsService;
|
||||
@Resource
|
||||
private ShopGoodsService shopGoodsService;
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@Override
|
||||
public boolean uploadExpressShippingInfo(ShopOrder order, ShopOrderDelivery orderDelivery, ShopExpress express) {
|
||||
if (order == null || order.getOrderId() == null) {
|
||||
return false;
|
||||
}
|
||||
if (orderDelivery == null || StrUtil.isBlank(orderDelivery.getExpressNo())) {
|
||||
log.warn("上传微信发货信息跳过:缺少运单号 - orderId={}", order.getOrderId());
|
||||
return false;
|
||||
}
|
||||
if (express == null || StrUtil.isBlank(express.getWxCode())) {
|
||||
log.warn("上传微信发货信息跳过:缺少微信快递公司编码(wxCode) - orderId={}", order.getOrderId());
|
||||
return false;
|
||||
}
|
||||
|
||||
List<ShippingListBean> shippingList = new ArrayList<>();
|
||||
ShippingListBean item = new ShippingListBean();
|
||||
item.setTrackingNo(orderDelivery.getExpressNo());
|
||||
item.setExpressCompany(express.getWxCode());
|
||||
item.setItemDesc(buildItemDesc(order.getOrderId()));
|
||||
shippingList.add(item);
|
||||
|
||||
return doUpload(order, LOGISTICS_TYPE_PHYSICAL, DELIVERY_MODE_EXPRESS, shippingList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean uploadNoLogisticsShippingInfo(ShopOrder order) {
|
||||
if (order == null || order.getOrderId() == null) {
|
||||
return false;
|
||||
}
|
||||
// 无需物流情况下通常不需要 shipping_list
|
||||
return doUpload(order, LOGISTICS_TYPE_NO_LOGISTICS, DELIVERY_MODE_NO_LOGISTICS, Collections.emptyList());
|
||||
}
|
||||
|
||||
private boolean doUpload(ShopOrder order, int logisticsType, int deliveryMode, List<ShippingListBean> shippingList) {
|
||||
// 仅对微信支付订单尝试同步(微信后台“待发货”来自微信支付交易)
|
||||
if (!ObjectUtil.equals(order.getPayType(), 1) && !ObjectUtil.equals(order.getPayType(), 102)) {
|
||||
return false;
|
||||
}
|
||||
if (!Boolean.TRUE.equals(order.getPayStatus())) {
|
||||
return false;
|
||||
}
|
||||
if (order.getTenantId() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// payer openid:必须是下单用户,不是后台操作员
|
||||
User buyer = userService.getByIdIgnoreTenant(order.getUserId());
|
||||
if (buyer == null || StrUtil.isBlank(buyer.getOpenid())) {
|
||||
log.warn("上传微信发货信息失败:买家openid为空 - orderId={}, userId={}", order.getOrderId(), order.getUserId());
|
||||
return false;
|
||||
}
|
||||
|
||||
String accessToken;
|
||||
try {
|
||||
accessToken = wxMiniappAccessTokenService.getAccessToken(order.getTenantId());
|
||||
} catch (Exception e) {
|
||||
log.error("获取小程序access_token失败 - orderId={}, tenantId={}", order.getOrderId(), order.getTenantId(), e);
|
||||
return false;
|
||||
}
|
||||
|
||||
OrderKeyBean orderKey = buildOrderKey(order);
|
||||
if (orderKey == null) {
|
||||
log.warn("上传微信发货信息跳过:无法构建order_key - orderId={}", order.getOrderId());
|
||||
return false;
|
||||
}
|
||||
|
||||
WxMaOrderShippingInfoUploadRequest uploadRequest = new WxMaOrderShippingInfoUploadRequest();
|
||||
uploadRequest.setOrderKey(orderKey);
|
||||
uploadRequest.setLogisticsType(logisticsType);
|
||||
uploadRequest.setDeliveryMode(deliveryMode);
|
||||
uploadRequest.setIsAllDelivered(true);
|
||||
if (shippingList != null && !shippingList.isEmpty()) {
|
||||
uploadRequest.setShippingList(shippingList);
|
||||
}
|
||||
uploadRequest.setUploadTime(new DateTime().toString(DatePattern.UTC_WITH_ZONE_OFFSET_PATTERN));
|
||||
|
||||
PayerBean payerBean = new PayerBean();
|
||||
payerBean.setOpenid(buyer.getOpenid());
|
||||
uploadRequest.setPayer(payerBean);
|
||||
|
||||
String url = WxMaApiUrlConstants.OrderShipping.UPLOAD_SHIPPING_INFO + "?access_token=" + accessToken;
|
||||
String body = GSON.toJson(uploadRequest);
|
||||
|
||||
try {
|
||||
String resp = HttpRequest.post(url)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(body)
|
||||
.timeout(10000)
|
||||
.execute()
|
||||
.body();
|
||||
JsonObject json = JsonParser.parseString(resp).getAsJsonObject();
|
||||
int errcode = json.has("errcode") ? json.get("errcode").getAsInt() : -1;
|
||||
String errmsg = json.has("errmsg") ? json.get("errmsg").getAsString() : resp;
|
||||
if (errcode == 0) {
|
||||
log.info("✅ 微信发货信息同步成功 - orderId={}, logisticsType={}, deliveryMode={}",
|
||||
order.getOrderId(), logisticsType, deliveryMode);
|
||||
return true;
|
||||
}
|
||||
log.error("❌ 微信发货信息同步失败 - orderId={}, errcode={}, errmsg={}, req={}",
|
||||
order.getOrderId(), errcode, errmsg, body);
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.error("❌ 微信发货信息同步异常 - orderId={}, req={}", order.getOrderId(), body, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private OrderKeyBean buildOrderKey(ShopOrder order) {
|
||||
if (StrUtil.isNotBlank(order.getTransactionId())) {
|
||||
OrderKeyBean key = new OrderKeyBean();
|
||||
key.setOrderNumberType(ORDER_NUMBER_TYPE_TRANSACTION_ID);
|
||||
key.setTransactionId(order.getTransactionId());
|
||||
return key;
|
||||
}
|
||||
|
||||
// transactionId 为空时,尝试使用 out_trade_no + mchid
|
||||
if (StrUtil.isBlank(order.getOrderNo())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Payment payment = loadWechatPaymentConfig(order.getTenantId());
|
||||
if (payment == null || StrUtil.isBlank(payment.getMchId())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
OrderKeyBean key = new OrderKeyBean();
|
||||
key.setOrderNumberType(ORDER_NUMBER_TYPE_OUT_TRADE_NO);
|
||||
key.setOutTradeNo(order.getOrderNo());
|
||||
key.setMchId(payment.getMchId());
|
||||
return key;
|
||||
}
|
||||
|
||||
private Payment loadWechatPaymentConfig(Integer tenantId) {
|
||||
if (tenantId == null) {
|
||||
return null;
|
||||
}
|
||||
// 与微信支付回调一致:Payment:1:{tenantId}
|
||||
String key = "Payment:1:" + tenantId;
|
||||
try {
|
||||
return redisUtil.get(key, Payment.class);
|
||||
} catch (Exception e) {
|
||||
log.warn("读取支付配置失败 - key={}", key, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String buildItemDesc(Integer orderId) {
|
||||
try {
|
||||
List<ShopOrderGoods> orderGoodsList = shopOrderGoodsService.getListByOrderId(orderId);
|
||||
if (orderGoodsList == null || orderGoodsList.isEmpty()) {
|
||||
return "订单商品";
|
||||
}
|
||||
ShopGoods shopGoods = shopGoodsService.getById(orderGoodsList.get(0).getGoodsId());
|
||||
String itemDesc = shopGoods != null && StrUtil.isNotBlank(shopGoods.getName()) ? shopGoods.getName() : "订单商品";
|
||||
if (orderGoodsList.size() > 1) {
|
||||
itemDesc += "等" + orderGoodsList.size() + "件商品";
|
||||
}
|
||||
return itemDesc;
|
||||
} catch (Exception e) {
|
||||
log.warn("构建微信发货 item_desc 失败 - orderId={}", orderId, e);
|
||||
return "订单商品";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.gxwebsoft.cms.service.impl;
|
||||
|
||||
import com.gxwebsoft.cms.entity.CmsWebsite;
|
||||
import com.gxwebsoft.cms.mapper.CmsWebsiteMapper;
|
||||
import com.gxwebsoft.cms.service.CmsNavigationService;
|
||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||
import com.gxwebsoft.shop.vo.ShopVo;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class CmsWebsiteServiceImplCacheTest {
|
||||
|
||||
@Test
|
||||
void getSiteInfo_cacheValueIsJsonNull_shouldFallbackToDb() throws Exception {
|
||||
Integer tenantId = 1;
|
||||
|
||||
RedisUtil redisUtil = mock(RedisUtil.class);
|
||||
CmsNavigationService cmsNavigationService = mock(CmsNavigationService.class);
|
||||
CmsWebsiteMapper cmsWebsiteMapper = mock(CmsWebsiteMapper.class);
|
||||
|
||||
// 历史缓存可能存在字符串 "null";Jackson 解析后会得到 null,需回源 DB。
|
||||
when(redisUtil.get("SiteInfo:" + tenantId)).thenReturn("null");
|
||||
when(cmsNavigationService.listRel(any())).thenReturn(Collections.emptyList());
|
||||
|
||||
CmsWebsite website = new CmsWebsite();
|
||||
website.setTenantId(tenantId);
|
||||
website.setTenantName("tenant");
|
||||
website.setWebsiteName("site");
|
||||
when(cmsWebsiteMapper.getByTenantId(tenantId)).thenReturn(website);
|
||||
|
||||
CmsWebsiteServiceImpl service = new CmsWebsiteServiceImpl();
|
||||
setField(service, "redisUtil", redisUtil);
|
||||
setField(service, "cmsNavigationService", cmsNavigationService);
|
||||
setField(service, "baseMapper", cmsWebsiteMapper);
|
||||
|
||||
ShopVo vo = service.getSiteInfo(tenantId);
|
||||
|
||||
assertNotNull(vo);
|
||||
verify(redisUtil).delete("SiteInfo:" + tenantId);
|
||||
verify(cmsWebsiteMapper).getByTenantId(tenantId);
|
||||
}
|
||||
|
||||
private static void setField(Object target, String fieldName, Object value) throws Exception {
|
||||
Class<?> c = target.getClass();
|
||||
while (c != null) {
|
||||
try {
|
||||
Field f = c.getDeclaredField(fieldName);
|
||||
f.setAccessible(true);
|
||||
f.set(target, value);
|
||||
return;
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
c = c.getSuperclass();
|
||||
}
|
||||
}
|
||||
throw new NoSuchFieldException(fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user