feat(generator): 添加代码生成器模板和AI聊天功能
- 新增.gitignore文件配置忽略规则 - 添加Taro页面配置模板add.config.ts.btl - 添加Taro页面组件模板add.tsx.btl用于动态表单生成 - 实现AiController提供AI聊天消息处理功能 - 集成WebSocket实现AI消息流式传输 - 添加支付宝支付配置工具类AlipayConfigUtil - 创建支付宝参数实体AlipayParam - 集成阿里云短信发送工具AliYunSender - 添加阿里云机器翻译工具AliyunTranslateUtil - 完善API响应结果包装类ApiResult - 配置多环境应用参数application.yml - 添加CMS生产环境配置application-cms.yml - 添加开发环境配置application-dev.yml - 添加生产环境配置application-prod.yml
This commit is contained in:
@@ -0,0 +1,313 @@
|
|||||||
|
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.core.exception.BusinessException;
|
||||||
|
import com.gxwebsoft.common.system.entity.User;
|
||||||
|
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||||
|
import com.gxwebsoft.glt.param.GltTicketOrderDeliveredParam;
|
||||||
|
import com.gxwebsoft.glt.param.GltTicketOrderParam;
|
||||||
|
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopStoreRider;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopUserAddress;
|
||||||
|
import com.gxwebsoft.shop.service.ShopStoreFenceService;
|
||||||
|
import com.gxwebsoft.shop.service.ShopStoreRiderService;
|
||||||
|
import com.gxwebsoft.shop.service.ShopUserAddressService;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
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;
|
||||||
|
@Resource
|
||||||
|
private ShopUserAddressService shopUserAddressService;
|
||||||
|
@Resource
|
||||||
|
private ShopStoreFenceService shopStoreFenceService;
|
||||||
|
@Resource
|
||||||
|
private ShopStoreRiderService shopStoreRiderService;
|
||||||
|
|
||||||
|
@Operation(summary = "分页查询送水订单")
|
||||||
|
@GetMapping("/page")
|
||||||
|
public ApiResult<PageResult<GltTicketOrder>> page(GltTicketOrderParam param) {
|
||||||
|
// 使用关联查询
|
||||||
|
return success(gltTicketOrderService.pageRel(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
@Operation(summary = "配送员端:分页查询我的送水订单")
|
||||||
|
@GetMapping("/rider/page")
|
||||||
|
public ApiResult<PageResult<GltTicketOrder>> riderPage(GltTicketOrderParam param) {
|
||||||
|
User loginUser = getLoginUser();
|
||||||
|
if (loginUser == null) {
|
||||||
|
return fail("请先登录", null);
|
||||||
|
}
|
||||||
|
Integer tenantId = getTenantId();
|
||||||
|
param.setTenantId(tenantId);
|
||||||
|
// 仅允许配送员访问
|
||||||
|
requireActiveRider(loginUser.getUserId(), tenantId);
|
||||||
|
|
||||||
|
param.setRiderId(loginUser.getUserId());
|
||||||
|
if (param.getDeliveryStatus() == null) {
|
||||||
|
param.setDeliveryStatus(GltTicketOrderService.DELIVERY_STATUS_WAITING);
|
||||||
|
}
|
||||||
|
// 配送员端默认按期望配送时间优先
|
||||||
|
if (StrUtil.isBlank(param.getSort())) {
|
||||||
|
param.setSort("sendTime asc, createTime desc");
|
||||||
|
}
|
||||||
|
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("请先登录");
|
||||||
|
}
|
||||||
|
if (gltTicketOrder == null) {
|
||||||
|
return fail("订单参数不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 地址快照:把用户关联的“详细地址”落到 glt_ticket_order.address,避免用户后续修改地址导致历史订单丢失
|
||||||
|
ShopUserAddress userAddress = null;
|
||||||
|
if (gltTicketOrder.getAddressId() != null) {
|
||||||
|
userAddress = shopUserAddressService.getByIdRel(gltTicketOrder.getAddressId());
|
||||||
|
} else {
|
||||||
|
userAddress = shopUserAddressService.getDefaultAddress(loginUser.getUserId());
|
||||||
|
}
|
||||||
|
if (userAddress == null) {
|
||||||
|
return fail("请先添加收货地址");
|
||||||
|
}
|
||||||
|
if (!loginUser.getUserId().equals(userAddress.getUserId())) {
|
||||||
|
return fail("收货地址不存在或无权限");
|
||||||
|
}
|
||||||
|
if (loginUser.getTenantId() != null && userAddress.getTenantId() != null
|
||||||
|
&& !loginUser.getTenantId().equals(userAddress.getTenantId())) {
|
||||||
|
return fail("收货地址不存在或无权限");
|
||||||
|
}
|
||||||
|
gltTicketOrder.setAddressId(userAddress.getId());
|
||||||
|
gltTicketOrder.setAddress(buildAddressSnapshot(userAddress));
|
||||||
|
|
||||||
|
// 下单时校验配送范围(电子围栏):不信任前端传经纬度,使用地址表坐标校验
|
||||||
|
if (StrUtil.isBlank(userAddress.getLat()) || StrUtil.isBlank(userAddress.getLng())) {
|
||||||
|
return fail("收货地址坐标缺失,请重新选择收货地址");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
double lat = Double.parseDouble(userAddress.getLat().trim());
|
||||||
|
double lng = Double.parseDouble(userAddress.getLng().trim());
|
||||||
|
shopStoreFenceService.validatePointInEnabledFences(loginUser.getTenantId(), lng, lat);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return fail(e.getMessage() == null ? "收货地址不在配送范围内" : e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
gltTicketOrderService.createWithWriteOff(gltTicketOrder, loginUser.getUserId(), loginUser.getTenantId());
|
||||||
|
return success("下单成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
@Operation(summary = "配送员接单")
|
||||||
|
@PostMapping("/{id}/accept")
|
||||||
|
public ApiResult<?> accept(@PathVariable("id") Integer id) {
|
||||||
|
User loginUser = getLoginUser();
|
||||||
|
if (loginUser == null) {
|
||||||
|
return fail("请先登录");
|
||||||
|
}
|
||||||
|
Integer tenantId = getTenantId();
|
||||||
|
requireActiveRider(loginUser.getUserId(), tenantId);
|
||||||
|
gltTicketOrderService.accept(id, loginUser.getUserId(), tenantId);
|
||||||
|
return success("接单成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
@Operation(summary = "配送员开始配送")
|
||||||
|
@PostMapping("/{id}/start")
|
||||||
|
public ApiResult<?> start(@PathVariable("id") Integer id) {
|
||||||
|
User loginUser = getLoginUser();
|
||||||
|
if (loginUser == null) {
|
||||||
|
return fail("请先登录");
|
||||||
|
}
|
||||||
|
Integer tenantId = getTenantId();
|
||||||
|
requireActiveRider(loginUser.getUserId(), tenantId);
|
||||||
|
gltTicketOrderService.start(id, loginUser.getUserId(), tenantId);
|
||||||
|
return success("开始配送");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
@Operation(summary = "配送员确认送达")
|
||||||
|
@PostMapping("/{id}/delivered")
|
||||||
|
public ApiResult<?> delivered(@PathVariable("id") Integer id,
|
||||||
|
@RequestBody(required = false) GltTicketOrderDeliveredParam body) {
|
||||||
|
User loginUser = getLoginUser();
|
||||||
|
if (loginUser == null) {
|
||||||
|
return fail("请先登录");
|
||||||
|
}
|
||||||
|
Integer tenantId = getTenantId();
|
||||||
|
requireActiveRider(loginUser.getUserId(), tenantId);
|
||||||
|
String sendEndImg = body == null ? null : body.getSendEndImg();
|
||||||
|
// 配送员提成结算:在 service 内部按“拍照上传/用户确认收货”规则幂等处理。
|
||||||
|
gltTicketOrderService.delivered(id, loginUser.getUserId(), tenantId, sendEndImg);
|
||||||
|
return success("确认送达");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("isAuthenticated()")
|
||||||
|
@Operation(summary = "用户确认收货")
|
||||||
|
@PostMapping("/{id}/confirm-receive")
|
||||||
|
public ApiResult<?> confirmReceive(@PathVariable("id") Integer id) {
|
||||||
|
User loginUser = getLoginUser();
|
||||||
|
if (loginUser == null) {
|
||||||
|
return fail("请先登录");
|
||||||
|
}
|
||||||
|
// 配送员提成结算:在 service 内部按规则幂等处理。
|
||||||
|
gltTicketOrderService.confirmReceive(id, loginUser.getUserId(), getTenantId());
|
||||||
|
return success("确认收货成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShopStoreRider requireActiveRider(Integer userId, Integer tenantId) {
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException("请先登录");
|
||||||
|
}
|
||||||
|
if (tenantId == null) {
|
||||||
|
throw new BusinessException("租户信息缺失");
|
||||||
|
}
|
||||||
|
ShopStoreRider rider = shopStoreRiderService.getOne(new LambdaQueryWrapper<ShopStoreRider>()
|
||||||
|
.eq(ShopStoreRider::getUserId, userId)
|
||||||
|
.eq(ShopStoreRider::getTenantId, tenantId)
|
||||||
|
.eq(ShopStoreRider::getIsDelete, 0)
|
||||||
|
.last("limit 1"));
|
||||||
|
if (rider == null) {
|
||||||
|
throw new BusinessException("非配送员,无权限操作");
|
||||||
|
}
|
||||||
|
if (rider.getStatus() == null || rider.getStatus() != 1) {
|
||||||
|
throw new BusinessException("配送员已禁用");
|
||||||
|
}
|
||||||
|
return rider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildAddressSnapshot(ShopUserAddress addr) {
|
||||||
|
if (addr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(addr.getFullAddress())) {
|
||||||
|
return addr.getFullAddress();
|
||||||
|
}
|
||||||
|
// 兼容旧数据:fullAddress 为空时,拼接省市区 + 详细地址
|
||||||
|
return StrUtil.blankToDefault(
|
||||||
|
StrUtil.join("",
|
||||||
|
StrUtil.nullToEmpty(addr.getProvince()),
|
||||||
|
StrUtil.nullToEmpty(addr.getCity()),
|
||||||
|
StrUtil.nullToEmpty(addr.getRegion()),
|
||||||
|
StrUtil.nullToEmpty(addr.getAddress())
|
||||||
|
),
|
||||||
|
addr.getAddress()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('glt:gltTicketOrder:update')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "修改送水订单")
|
||||||
|
@PutMapping()
|
||||||
|
public ApiResult<?> update(@RequestBody GltTicketOrder gltTicketOrder) {
|
||||||
|
if (gltTicketOrderService.updateById(gltTicketOrder)) {
|
||||||
|
Integer tenantId = getTenantId();
|
||||||
|
// 后台指派配送员(直接改 riderId)时,同步商城订单为“已发货”(deliveryStatus=20)
|
||||||
|
if (gltTicketOrder != null
|
||||||
|
&& gltTicketOrder.getId() != null
|
||||||
|
&& gltTicketOrder.getRiderId() != null
|
||||||
|
&& gltTicketOrder.getRiderId() > 0) {
|
||||||
|
gltTicketOrderService.markShopOrderShippedAfterRiderAssigned(
|
||||||
|
gltTicketOrder.getId(),
|
||||||
|
tenantId,
|
||||||
|
gltTicketOrder.getRiderId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 后台直接改“已完成”(deliveryStatus=40)时,同步商城订单为“已完成”(orderStatus=1)
|
||||||
|
if (gltTicketOrder != null
|
||||||
|
&& gltTicketOrder.getId() != null
|
||||||
|
&& gltTicketOrder.getDeliveryStatus() != null
|
||||||
|
&& gltTicketOrder.getDeliveryStatus() == GltTicketOrderService.DELIVERY_STATUS_FINISHED) {
|
||||||
|
gltTicketOrderService.markShopOrderCompletedAfterTicketFinished(gltTicketOrder.getId(), tenantId);
|
||||||
|
}
|
||||||
|
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("删除失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
214
src/main/java/com/gxwebsoft/glt/entity/GltTicketOrder.java
Normal file
214
src/main/java/com/gxwebsoft/glt/entity/GltTicketOrder.java
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
package com.gxwebsoft.glt.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
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 = "订单编号")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String orderNo;
|
||||||
|
|
||||||
|
@Schema(description = "门店ID")
|
||||||
|
private Integer storeId;
|
||||||
|
|
||||||
|
@Schema(description = "门店名称")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
@Schema(description = "门店地址")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String storeAddress;
|
||||||
|
|
||||||
|
@Schema(description = "门店手机号")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String storePhone;
|
||||||
|
|
||||||
|
@Schema(description = "配送员")
|
||||||
|
private Integer riderId;
|
||||||
|
|
||||||
|
@Schema(description = "配送员名称")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String riderName;
|
||||||
|
|
||||||
|
@Schema(description = "配送员手机号")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String riderPhone;
|
||||||
|
|
||||||
|
@Schema(description = "仓库ID")
|
||||||
|
private Integer warehouseId;
|
||||||
|
|
||||||
|
@Schema(description = "仓库名称")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String warehouseName;
|
||||||
|
|
||||||
|
@Schema(description = "仓库地址")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String warehouseAddress;
|
||||||
|
|
||||||
|
@Schema(description = "仓库手机号")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String warehousePhone;
|
||||||
|
|
||||||
|
@Schema(description = "关联收货地址")
|
||||||
|
private Integer addressId;
|
||||||
|
|
||||||
|
@Schema(description = "收货地址")
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
@Schema(description = "省")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String province;
|
||||||
|
|
||||||
|
@Schema(description = "市")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
@Schema(description = "区")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String region;
|
||||||
|
|
||||||
|
@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 = "配送状态:10待配送、20配送中、30待客户确认、40已完成")
|
||||||
|
private Integer deliveryStatus;
|
||||||
|
|
||||||
|
@Schema(description = "开始配送时间")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime sendStartTime;
|
||||||
|
|
||||||
|
@Schema(description = "确认送达时间")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime sendEndTime;
|
||||||
|
|
||||||
|
@Schema(description = "送达拍照留档图片URL")
|
||||||
|
private String sendEndImg;
|
||||||
|
|
||||||
|
@Schema(description = "客户确认收货时间")
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private LocalDateTime receiveConfirmTime;
|
||||||
|
|
||||||
|
@Schema(description = "确认方式:10客户手动确认、20配送照片自动确认、30超时自动确认")
|
||||||
|
private Integer receiveConfirmType;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
@Schema(description = "昵称")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
@Schema(description = "手机号")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Schema(description = "头像")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
@Schema(description = "收货人姓名")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String receiverName;
|
||||||
|
|
||||||
|
@Schema(description = "收货人手机号")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String receiverPhone;
|
||||||
|
|
||||||
|
@Schema(description = "收货省")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String receiverProvince;
|
||||||
|
|
||||||
|
@Schema(description = "收货市")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String receiverCity;
|
||||||
|
|
||||||
|
@Schema(description = "收货区")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String receiverRegion;
|
||||||
|
|
||||||
|
@Schema(description = "收货详细地址")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String receiverAddress;
|
||||||
|
|
||||||
|
@Schema(description = "收货完整地址")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String receiverFullAddress;
|
||||||
|
|
||||||
|
@Schema(description = "收货纬度")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String receiverLat;
|
||||||
|
|
||||||
|
@Schema(description = "收货经度")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String receiverLng;
|
||||||
|
|
||||||
|
@Schema(description = "门店经纬度(lng,lat)")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String storeLngAndLat;
|
||||||
|
|
||||||
|
@Schema(description = "仓库经纬度(lng,lat)")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String warehouseLngAndLat;
|
||||||
|
|
||||||
|
@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);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?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.*, b.name as storeName, b.address as storeAddress, b.phone as storePhone, b.lng_and_lat as storeLngAndLat,
|
||||||
|
w.name as warehouseName, w.address as warehouseAddress, w.phone as warehousePhone, w.lng_and_lat as warehouseLngAndLat,
|
||||||
|
c.real_name as riderName, c.mobile as riderPhone,
|
||||||
|
u.nickname, u.phone, u.avatar,
|
||||||
|
d.name as receiverName, d.phone as receiverPhone,
|
||||||
|
d.province as receiverProvince, d.city as receiverCity, d.region as receiverRegion,
|
||||||
|
d.address as receiverAddress, d.full_address as receiverFullAddress, d.lat as receiverLat, d.lng as receiverLng,
|
||||||
|
COALESCE(o.order_no, f.order_no) as orderNo
|
||||||
|
FROM glt_ticket_order a
|
||||||
|
LEFT JOIN shop_store b ON a.store_id = b.id
|
||||||
|
LEFT JOIN shop_store_warehouse w ON a.warehouse_id = w.id
|
||||||
|
LEFT JOIN shop_store_rider c ON a.rider_id = c.user_id
|
||||||
|
LEFT JOIN gxwebsoft_core.sys_user u ON a.user_id = u.user_id
|
||||||
|
LEFT JOIN shop_user_address d ON a.address_id = d.id
|
||||||
|
LEFT JOIN glt_user_ticket f ON a.user_ticket_id = f.id
|
||||||
|
LEFT JOIN shop_order o ON f.order_id = o.order_id AND f.tenant_id = o.tenant_id AND o.deleted = 0
|
||||||
|
|
||||||
|
<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.deliveryStatus != null">
|
||||||
|
AND a.delivery_status = #{param.deliveryStatus}
|
||||||
|
</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.tenantId != null">
|
||||||
|
AND a.tenant_id = #{param.tenantId}
|
||||||
|
</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.address LIKE CONCAT('%', #{param.keywords}, '%')
|
||||||
|
OR a.buyer_remarks LIKE CONCAT('%', #{param.keywords}, '%')
|
||||||
|
OR a.comments LIKE CONCAT('%', #{param.keywords}, '%')
|
||||||
|
OR b.name LIKE CONCAT('%', #{param.keywords}, '%')
|
||||||
|
OR u.nickname LIKE CONCAT('%', #{param.keywords}, '%')
|
||||||
|
OR u.phone 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>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.gxwebsoft.glt.param;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配送员确认送达参数
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class GltTicketOrderDeliveredParam implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "送达拍照留档图片URL")
|
||||||
|
private String sendEndImg;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
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 = "配送状态:10待配送、20配送中、30待客户确认、40已完成")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer deliveryStatus;
|
||||||
|
|
||||||
|
@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,109 @@
|
|||||||
|
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;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 送水订单Service
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-02-05 18:50:20
|
||||||
|
*/
|
||||||
|
public interface GltTicketOrderService extends IService<GltTicketOrder> {
|
||||||
|
|
||||||
|
int DELIVERY_STATUS_WAITING = 10;
|
||||||
|
int DELIVERY_STATUS_DELIVERING = 20;
|
||||||
|
int DELIVERY_STATUS_WAIT_CONFIRM = 30;
|
||||||
|
int DELIVERY_STATUS_FINISHED = 40;
|
||||||
|
|
||||||
|
int RECEIVE_CONFIRM_TYPE_MANUAL = 10;
|
||||||
|
int RECEIVE_CONFIRM_TYPE_PHOTO = 20;
|
||||||
|
int RECEIVE_CONFIRM_TYPE_TIMEOUT = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页关联查询
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配送员接单(原子):仅当 riderId 为空(或0)且 deliveryStatus=10 时写入 riderId。
|
||||||
|
*/
|
||||||
|
void accept(Integer id, Integer riderId, Integer tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指派/接单成功后,同步关联商城订单发货状态为“已发货”(deliveryStatus=20)。
|
||||||
|
*
|
||||||
|
* <p>用于后台指派配送员(不走接单接口)等场景的状态兜底同步。</p>
|
||||||
|
*/
|
||||||
|
void markShopOrderShippedAfterRiderAssigned(Integer ticketOrderId, Integer tenantId, Integer riderId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 送水订单完成后,同步关联商城订单为“已完成”(orderStatus=1)。
|
||||||
|
*
|
||||||
|
* <p>用于后台直接改 deliveryStatus=40 等不经过 confirmReceive/autoConfirmTimeout 的兜底同步。</p>
|
||||||
|
*/
|
||||||
|
void markShopOrderCompletedAfterTicketFinished(Integer ticketOrderId, Integer tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配送员开始配送:10 -> 20,并写 sendStartTime。
|
||||||
|
*/
|
||||||
|
void start(Integer id, Integer riderId, Integer tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配送员确认送达:20 -> 30,并写 sendEndTime / sendEndImg。
|
||||||
|
*/
|
||||||
|
void delivered(Integer id, Integer riderId, Integer tenantId, String sendEndImg);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户确认收货:30 -> 40,并写 receiveConfirmTime / receiveConfirmType=10。
|
||||||
|
*/
|
||||||
|
void confirmReceive(Integer id, Integer userId, Integer tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超时自动确认收货:
|
||||||
|
* - 扫描已送达待确认(30)且送达时间(sendEndTime)超过指定小时数的订单
|
||||||
|
* - 自动置为已完成(40),并写 receiveConfirmTime / receiveConfirmType=30
|
||||||
|
*
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
* @param now 当前时间
|
||||||
|
* @param timeoutHours 超时小时数(如24)
|
||||||
|
* @param batchSize 每次处理条数上限
|
||||||
|
* @return 本次自动确认条数
|
||||||
|
*/
|
||||||
|
int autoConfirmTimeout(Integer tenantId, LocalDateTime now, int timeoutHours, int batchSize);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.gxwebsoft.glt.service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 冻结水票自动释放(到达 release_time 后执行:frozen -> available)。
|
||||||
|
*/
|
||||||
|
public interface GltUserTicketAutoReleaseService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放到期的冻结水票
|
||||||
|
*
|
||||||
|
* @param now 当前时间(用于测试/可控时钟)
|
||||||
|
* @param batchSize 单次处理上限
|
||||||
|
* @return 成功释放的条数
|
||||||
|
*/
|
||||||
|
int releaseDue(LocalDateTime now, int batchSize);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放到期的冻结水票(使用系统当前时间)
|
||||||
|
*/
|
||||||
|
default int releaseDue(int batchSize) {
|
||||||
|
return releaseDue(LocalDateTime.now(), batchSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,879 @@
|
|||||||
|
package com.gxwebsoft.glt.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
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.common.system.entity.User;
|
||||||
|
import com.gxwebsoft.common.system.mapper.UserMapper;
|
||||||
|
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 com.gxwebsoft.shop.entity.ShopDealerCapital;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopDealerUser;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopOrder;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopOrderGoods;
|
||||||
|
import com.gxwebsoft.shop.service.ShopDealerCapitalService;
|
||||||
|
import com.gxwebsoft.shop.service.ShopDealerUserService;
|
||||||
|
import com.gxwebsoft.shop.service.ShopOrderGoodsService;
|
||||||
|
import com.gxwebsoft.shop.service.ShopOrderService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 送水订单Service实现
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-02-05 18:50:20
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class GltTicketOrderServiceImpl extends ServiceImpl<GltTicketOrderMapper, GltTicketOrder> implements GltTicketOrderService {
|
||||||
|
|
||||||
|
public static final int CHANGE_TYPE_WRITE_OFF = 20;
|
||||||
|
private static final BigDecimal RIDER_UNIT_COMMISSION = new BigDecimal("0.10");
|
||||||
|
private static final int RIDER_COMMISSION_SCALE = 2;
|
||||||
|
private static final int TENANT_ID_10584 = 10584;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GltUserTicketMapper gltUserTicketMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GltUserTicketService gltUserTicketService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GltUserTicketLogService gltUserTicketLogService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TransactionTemplate transactionTemplate;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ShopDealerUserService shopDealerUserService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ShopDealerCapitalService shopDealerCapitalService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private UserMapper userMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ShopOrderService shopOrderService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ShopOrderGoodsService shopOrderGoodsService;
|
||||||
|
|
||||||
|
@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.getDeliveryStatus() == null) {
|
||||||
|
gltTicketOrder.setDeliveryStatus(DELIVERY_STATUS_WAITING);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void accept(Integer id, Integer riderId, Integer tenantId) {
|
||||||
|
if (id == null) {
|
||||||
|
throw new BusinessException("订单id不能为空");
|
||||||
|
}
|
||||||
|
if (riderId == null) {
|
||||||
|
throw new BusinessException("配送员信息缺失");
|
||||||
|
}
|
||||||
|
if (tenantId == null) {
|
||||||
|
throw new BusinessException("租户信息缺失");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原子接单:避免并发抢单
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
boolean ok = this.lambdaUpdate()
|
||||||
|
.set(GltTicketOrder::getRiderId, riderId)
|
||||||
|
.set(GltTicketOrder::getUpdateTime, now)
|
||||||
|
.eq(GltTicketOrder::getId, id)
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.and(w -> w.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_WAITING)
|
||||||
|
.or().isNull(GltTicketOrder::getDeliveryStatus))
|
||||||
|
.and(w -> w.isNull(GltTicketOrder::getRiderId).or().eq(GltTicketOrder::getRiderId, 0))
|
||||||
|
.update();
|
||||||
|
if (ok) {
|
||||||
|
// 接单成功后,同步商城订单发货状态:10未发货 -> 20已发货
|
||||||
|
updateShopOrderDeliveryStatusAfterAccept(id, tenantId, riderId, now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回查给出更明确的错误
|
||||||
|
GltTicketOrder order = this.lambdaQuery()
|
||||||
|
.eq(GltTicketOrder::getId, id)
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.one();
|
||||||
|
if (order == null) {
|
||||||
|
throw new BusinessException("订单不存在");
|
||||||
|
}
|
||||||
|
if (order.getRiderId() != null && order.getRiderId() > 0) {
|
||||||
|
throw new BusinessException("订单已被其他配送员接单");
|
||||||
|
}
|
||||||
|
throw new BusinessException("订单状态不允许接单");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markShopOrderShippedAfterRiderAssigned(Integer ticketOrderId, Integer tenantId, Integer riderId) {
|
||||||
|
updateShopOrderDeliveryStatusAfterAccept(ticketOrderId, tenantId, riderId, LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void markShopOrderCompletedAfterTicketFinished(Integer ticketOrderId, Integer tenantId) {
|
||||||
|
updateShopOrderOrderStatusAfterTicketFinished(ticketOrderId, tenantId, LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateShopOrderDeliveryStatusAfterAccept(Integer ticketOrderId, Integer tenantId, Integer riderId, LocalDateTime now) {
|
||||||
|
if (ticketOrderId == null || tenantId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到关联水票的商城订单(glt_user_ticket.orderId / orderNo)
|
||||||
|
GltTicketOrder ticketOrder = this.lambdaQuery()
|
||||||
|
.select(GltTicketOrder::getId, GltTicketOrder::getUserTicketId, GltTicketOrder::getRiderId)
|
||||||
|
.eq(GltTicketOrder::getId, ticketOrderId)
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
if (ticketOrder == null || ticketOrder.getUserTicketId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer actualRiderId = (riderId != null && riderId > 0) ? riderId : ticketOrder.getRiderId();
|
||||||
|
|
||||||
|
GltUserTicket userTicket = gltUserTicketService.getOne(
|
||||||
|
new LambdaQueryWrapper<GltUserTicket>()
|
||||||
|
.eq(GltUserTicket::getTenantId, tenantId)
|
||||||
|
.eq(GltUserTicket::getDeleted, 0)
|
||||||
|
.eq(GltUserTicket::getId, ticketOrder.getUserTicketId())
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
if (userTicket == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer shopOrderId = userTicket.getOrderId();
|
||||||
|
String shopOrderNo = userTicket.getOrderNo();
|
||||||
|
boolean resolvedByOrderGoodsId = false;
|
||||||
|
// 兼容历史数据:部分水票可能只写了 orderGoodsId(未写 orderId/orderNo),此处兜底通过 orderGoodsId 反查 ShopOrder.orderId。
|
||||||
|
if (shopOrderId == null && !StringUtils.hasText(shopOrderNo) && userTicket.getOrderGoodsId() != null) {
|
||||||
|
ShopOrderGoods og = shopOrderGoodsService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopOrderGoods>()
|
||||||
|
.select(ShopOrderGoods::getOrderId)
|
||||||
|
.eq(ShopOrderGoods::getTenantId, tenantId)
|
||||||
|
.eq(ShopOrderGoods::getId, userTicket.getOrderGoodsId())
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
if (og != null) {
|
||||||
|
shopOrderId = og.getOrderId();
|
||||||
|
resolvedByOrderGoodsId = shopOrderId != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shopOrderId == null && !StringUtils.hasText(shopOrderNo)) {
|
||||||
|
log.warn("同步商城订单发货状态失败:未找到关联商城订单 - tenantId={}, ticketOrderId={}, userTicketId={}, userTicket.orderId={}, userTicket.orderNo={}, userTicket.orderGoodsId={}",
|
||||||
|
tenantId, ticketOrderId, userTicket.getId(), userTicket.getOrderId(), userTicket.getOrderNo(), userTicket.getOrderGoodsId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 若是通过 orderGoodsId 兜底反查到 orderId,则顺便回填 glt_user_ticket.order_id/order_no,减少后续同步/查询依赖兜底分支。
|
||||||
|
if (resolvedByOrderGoodsId && userTicket.getOrderId() == null && shopOrderId != null) {
|
||||||
|
if (!StringUtils.hasText(shopOrderNo)) {
|
||||||
|
ShopOrder order = shopOrderService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopOrder>()
|
||||||
|
.select(ShopOrder::getOrderNo)
|
||||||
|
.eq(ShopOrder::getTenantId, tenantId)
|
||||||
|
.eq(ShopOrder::getDeleted, 0)
|
||||||
|
.eq(ShopOrder::getOrderId, shopOrderId)
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
if (order != null) {
|
||||||
|
shopOrderNo = order.getOrderNo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaUpdateWrapper<GltUserTicket> backfill = new LambdaUpdateWrapper<GltUserTicket>()
|
||||||
|
.eq(GltUserTicket::getTenantId, tenantId)
|
||||||
|
.eq(GltUserTicket::getDeleted, 0)
|
||||||
|
.eq(GltUserTicket::getId, userTicket.getId());
|
||||||
|
backfill.set(GltUserTicket::getOrderId, shopOrderId);
|
||||||
|
if (!StringUtils.hasText(userTicket.getOrderNo()) && StringUtils.hasText(shopOrderNo)) {
|
||||||
|
backfill.set(GltUserTicket::getOrderNo, shopOrderNo);
|
||||||
|
}
|
||||||
|
backfill.set(GltUserTicket::getUpdateTime, now);
|
||||||
|
try {
|
||||||
|
gltUserTicketService.update(backfill);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("回填水票关联商城订单信息失败(不影响主流程) - tenantId={}, userTicketId={}, orderId={}, orderNo={}",
|
||||||
|
tenantId, userTicket.getId(), shopOrderId, shopOrderNo, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaUpdateWrapper<ShopOrder> uw = new LambdaUpdateWrapper<ShopOrder>()
|
||||||
|
.eq(ShopOrder::getTenantId, tenantId)
|
||||||
|
.eq(ShopOrder::getDeleted, 0)
|
||||||
|
// deliveryStatus 已经是 20 时也可能需要补写/更新 riderId,因此条件包含 riderId 不一致的场景
|
||||||
|
.and(w -> {
|
||||||
|
w.ne(ShopOrder::getDeliveryStatus, 20).or().isNull(ShopOrder::getDeliveryStatus);
|
||||||
|
if (actualRiderId != null && actualRiderId > 0) {
|
||||||
|
w.or().ne(ShopOrder::getRiderId, actualRiderId).or().isNull(ShopOrder::getRiderId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.set(ShopOrder::getDeliveryStatus, 20)
|
||||||
|
.set(ShopOrder::getUpdateTime, now);
|
||||||
|
if (actualRiderId != null && actualRiderId > 0) {
|
||||||
|
uw.set(ShopOrder::getRiderId, actualRiderId);
|
||||||
|
}
|
||||||
|
if (shopOrderId != null) {
|
||||||
|
uw.eq(ShopOrder::getOrderId, shopOrderId);
|
||||||
|
} else {
|
||||||
|
uw.eq(ShopOrder::getOrderNo, shopOrderNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean updated = shopOrderService.update(uw);
|
||||||
|
if (updated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 幂等:若已是 20,则视为成功;否则记录日志便于排查关联关系/数据缺失
|
||||||
|
LambdaQueryWrapper<ShopOrder> qw = new LambdaQueryWrapper<ShopOrder>()
|
||||||
|
.eq(ShopOrder::getTenantId, tenantId)
|
||||||
|
.eq(ShopOrder::getDeleted, 0)
|
||||||
|
.eq(ShopOrder::getDeliveryStatus, 20);
|
||||||
|
if (actualRiderId != null && actualRiderId > 0) {
|
||||||
|
qw.eq(ShopOrder::getRiderId, actualRiderId);
|
||||||
|
}
|
||||||
|
if (shopOrderId != null) {
|
||||||
|
qw.eq(ShopOrder::getOrderId, shopOrderId);
|
||||||
|
} else {
|
||||||
|
qw.eq(ShopOrder::getOrderNo, shopOrderNo);
|
||||||
|
}
|
||||||
|
if (shopOrderService.count(qw) <= 0) {
|
||||||
|
log.warn("接单/指派成功但同步商城订单发货状态/配送员失败 - tenantId={}, ticketOrderId={}, riderId={}, shopOrderId={}, shopOrderNo={}",
|
||||||
|
tenantId, ticketOrderId, actualRiderId, shopOrderId, shopOrderNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(Integer id, Integer riderId, Integer tenantId) {
|
||||||
|
if (id == null) {
|
||||||
|
throw new BusinessException("订单id不能为空");
|
||||||
|
}
|
||||||
|
if (riderId == null) {
|
||||||
|
throw new BusinessException("配送员信息缺失");
|
||||||
|
}
|
||||||
|
if (tenantId == null) {
|
||||||
|
throw new BusinessException("租户信息缺失");
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
boolean ok = this.lambdaUpdate()
|
||||||
|
.set(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_DELIVERING)
|
||||||
|
.set(GltTicketOrder::getSendStartTime, now)
|
||||||
|
.set(GltTicketOrder::getUpdateTime, now)
|
||||||
|
.eq(GltTicketOrder::getId, id)
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.eq(GltTicketOrder::getRiderId, riderId)
|
||||||
|
.and(w -> w.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_WAITING)
|
||||||
|
.or().isNull(GltTicketOrder::getDeliveryStatus))
|
||||||
|
.update();
|
||||||
|
if (ok) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GltTicketOrder order = this.lambdaQuery()
|
||||||
|
.eq(GltTicketOrder::getId, id)
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.one();
|
||||||
|
if (order == null) {
|
||||||
|
throw new BusinessException("订单不存在");
|
||||||
|
}
|
||||||
|
if (!riderId.equals(order.getRiderId())) {
|
||||||
|
throw new BusinessException("无权限操作该订单");
|
||||||
|
}
|
||||||
|
if (order.getDeliveryStatus() != null && order.getDeliveryStatus() == DELIVERY_STATUS_DELIVERING) {
|
||||||
|
// 幂等:重复开始配送视为成功
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new BusinessException("订单状态不允许开始配送");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void delivered(Integer id, Integer riderId, Integer tenantId, String sendEndImg) {
|
||||||
|
if (id == null) {
|
||||||
|
throw new BusinessException("订单id不能为空");
|
||||||
|
}
|
||||||
|
if (riderId == null) {
|
||||||
|
throw new BusinessException("配送员信息缺失");
|
||||||
|
}
|
||||||
|
if (tenantId == null) {
|
||||||
|
throw new BusinessException("租户信息缺失");
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
var update = this.lambdaUpdate()
|
||||||
|
.set(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_WAIT_CONFIRM)
|
||||||
|
.set(GltTicketOrder::getSendEndTime, now)
|
||||||
|
.set(GltTicketOrder::getUpdateTime, now);
|
||||||
|
if (StringUtils.hasText(sendEndImg)) {
|
||||||
|
update.set(GltTicketOrder::getSendEndImg, sendEndImg);
|
||||||
|
}
|
||||||
|
boolean ok = update
|
||||||
|
.eq(GltTicketOrder::getId, id)
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.eq(GltTicketOrder::getRiderId, riderId)
|
||||||
|
.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_DELIVERING)
|
||||||
|
.update();
|
||||||
|
if (ok) {
|
||||||
|
// 配送员拍照上传送达后可触发提成结算(幂等,重复调用不会重复入账)
|
||||||
|
settleRiderCommissionIfEligible(id, tenantId, true);
|
||||||
|
// 配送员确认送达后,同步商城订单为“已完成”(orderStatus=1)
|
||||||
|
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GltTicketOrder order = this.lambdaQuery()
|
||||||
|
.eq(GltTicketOrder::getId, id)
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.one();
|
||||||
|
if (order == null) {
|
||||||
|
throw new BusinessException("订单不存在");
|
||||||
|
}
|
||||||
|
if (!riderId.equals(order.getRiderId())) {
|
||||||
|
throw new BusinessException("无权限操作该订单");
|
||||||
|
}
|
||||||
|
if (order.getDeliveryStatus() != null
|
||||||
|
&& (order.getDeliveryStatus() == DELIVERY_STATUS_WAIT_CONFIRM
|
||||||
|
|| order.getDeliveryStatus() == DELIVERY_STATUS_FINISHED)) {
|
||||||
|
// 幂等:重复送达视为成功
|
||||||
|
settleRiderCommissionIfEligible(id, tenantId, true);
|
||||||
|
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, LocalDateTime.now());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new BusinessException("订单状态不允许确认送达");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void confirmReceive(Integer id, Integer userId, Integer tenantId) {
|
||||||
|
if (id == null) {
|
||||||
|
throw new BusinessException("订单id不能为空");
|
||||||
|
}
|
||||||
|
if (userId == null) {
|
||||||
|
throw new BusinessException("请先登录");
|
||||||
|
}
|
||||||
|
if (tenantId == null) {
|
||||||
|
throw new BusinessException("租户信息缺失");
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
boolean ok = this.lambdaUpdate()
|
||||||
|
.set(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_FINISHED)
|
||||||
|
.set(GltTicketOrder::getReceiveConfirmTime, now)
|
||||||
|
.set(GltTicketOrder::getReceiveConfirmType, RECEIVE_CONFIRM_TYPE_MANUAL)
|
||||||
|
.set(GltTicketOrder::getUpdateTime, now)
|
||||||
|
.eq(GltTicketOrder::getId, id)
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.eq(GltTicketOrder::getUserId, userId)
|
||||||
|
.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_WAIT_CONFIRM)
|
||||||
|
.update();
|
||||||
|
if (ok) {
|
||||||
|
// 用户确认收货完成后触发配送员提成结算(幂等,重复调用不会重复入账)
|
||||||
|
settleRiderCommissionIfEligible(id, tenantId, false);
|
||||||
|
// 送水订单完成后,同步商城订单为“已完成”(orderStatus=1)
|
||||||
|
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GltTicketOrder order = this.lambdaQuery()
|
||||||
|
.eq(GltTicketOrder::getId, id)
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.one();
|
||||||
|
if (order == null) {
|
||||||
|
throw new BusinessException("订单不存在");
|
||||||
|
}
|
||||||
|
if (!userId.equals(order.getUserId())) {
|
||||||
|
throw new BusinessException("无权限操作该订单");
|
||||||
|
}
|
||||||
|
if (order.getDeliveryStatus() != null && order.getDeliveryStatus() == DELIVERY_STATUS_FINISHED) {
|
||||||
|
// 幂等:重复确认收货视为成功
|
||||||
|
settleRiderCommissionIfEligible(id, tenantId, false);
|
||||||
|
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, LocalDateTime.now());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new BusinessException("订单状态不允许确认收货");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int autoConfirmTimeout(Integer tenantId, LocalDateTime now, int timeoutHours, int batchSize) {
|
||||||
|
if (tenantId == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (now == null) {
|
||||||
|
now = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
int hours = Math.max(timeoutHours, 1);
|
||||||
|
int limit = Math.max(batchSize, 1);
|
||||||
|
LocalDateTime deadline = now.minusHours(hours);
|
||||||
|
|
||||||
|
List<GltTicketOrder> candidates = this.lambdaQuery()
|
||||||
|
.select(GltTicketOrder::getId)
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_WAIT_CONFIRM)
|
||||||
|
.isNotNull(GltTicketOrder::getSendEndTime)
|
||||||
|
.le(GltTicketOrder::getSendEndTime, deadline)
|
||||||
|
.orderByAsc(GltTicketOrder::getSendEndTime)
|
||||||
|
.orderByAsc(GltTicketOrder::getId)
|
||||||
|
.last("limit " + limit)
|
||||||
|
.list();
|
||||||
|
if (candidates == null || candidates.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int confirmed = 0;
|
||||||
|
for (GltTicketOrder item : candidates) {
|
||||||
|
Integer id = item != null ? item.getId() : null;
|
||||||
|
if (id == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final LocalDateTime nowFinal = now;
|
||||||
|
final LocalDateTime deadlineFinal = deadline;
|
||||||
|
Boolean ok = transactionTemplate.execute(status -> {
|
||||||
|
boolean updated = this.lambdaUpdate()
|
||||||
|
.set(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_FINISHED)
|
||||||
|
.set(GltTicketOrder::getReceiveConfirmTime, nowFinal)
|
||||||
|
.set(GltTicketOrder::getReceiveConfirmType, RECEIVE_CONFIRM_TYPE_TIMEOUT)
|
||||||
|
.set(GltTicketOrder::getUpdateTime, nowFinal)
|
||||||
|
.eq(GltTicketOrder::getId, id)
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.eq(GltTicketOrder::getDeliveryStatus, DELIVERY_STATUS_WAIT_CONFIRM)
|
||||||
|
.le(GltTicketOrder::getSendEndTime, deadlineFinal)
|
||||||
|
.update();
|
||||||
|
if (!updated) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 超时自动确认收货后,也按“完成”逻辑触发配送员提成结算(幂等)。
|
||||||
|
settleRiderCommissionIfEligible(id, tenantId, false);
|
||||||
|
updateShopOrderOrderStatusAfterTicketFinished(id, tenantId, nowFinal);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (Boolean.TRUE.equals(ok)) {
|
||||||
|
confirmed++;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("送水订单超时自动确认收货失败 - tenantId={}, ticketOrderId={}", tenantId, id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return confirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateShopOrderOrderStatusAfterTicketFinished(Integer ticketOrderId, Integer tenantId, LocalDateTime now) {
|
||||||
|
if (ticketOrderId == null || tenantId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (now == null) {
|
||||||
|
now = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 找到关联水票的商城订单(glt_user_ticket.orderId / orderNo)
|
||||||
|
GltTicketOrder ticketOrder = this.lambdaQuery()
|
||||||
|
.select(GltTicketOrder::getId, GltTicketOrder::getUserTicketId)
|
||||||
|
.eq(GltTicketOrder::getId, ticketOrderId)
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
if (ticketOrder == null || ticketOrder.getUserTicketId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GltUserTicket userTicket = gltUserTicketService.getOne(
|
||||||
|
new LambdaQueryWrapper<GltUserTicket>()
|
||||||
|
.eq(GltUserTicket::getTenantId, tenantId)
|
||||||
|
.eq(GltUserTicket::getDeleted, 0)
|
||||||
|
.eq(GltUserTicket::getId, ticketOrder.getUserTicketId())
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
if (userTicket == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer shopOrderId = userTicket.getOrderId();
|
||||||
|
String shopOrderNo = userTicket.getOrderNo();
|
||||||
|
boolean resolvedByOrderGoodsId = false;
|
||||||
|
// 兼容历史数据:部分水票可能只写了 orderGoodsId(未写 orderId/orderNo),此处兜底通过 orderGoodsId 反查 ShopOrder.orderId。
|
||||||
|
if (shopOrderId == null && !StringUtils.hasText(shopOrderNo) && userTicket.getOrderGoodsId() != null) {
|
||||||
|
ShopOrderGoods og = shopOrderGoodsService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopOrderGoods>()
|
||||||
|
.select(ShopOrderGoods::getOrderId)
|
||||||
|
.eq(ShopOrderGoods::getTenantId, tenantId)
|
||||||
|
.eq(ShopOrderGoods::getId, userTicket.getOrderGoodsId())
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
if (og != null) {
|
||||||
|
shopOrderId = og.getOrderId();
|
||||||
|
resolvedByOrderGoodsId = shopOrderId != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shopOrderId == null && !StringUtils.hasText(shopOrderNo)) {
|
||||||
|
log.warn("送水订单完成但未找到关联商城订单,无法同步完成状态 - tenantId={}, ticketOrderId={}, userTicketId={}, userTicket.orderId={}, userTicket.orderNo={}, userTicket.orderGoodsId={}",
|
||||||
|
tenantId, ticketOrderId, userTicket.getId(), userTicket.getOrderId(), userTicket.getOrderNo(), userTicket.getOrderGoodsId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 若是通过 orderGoodsId 兜底反查到 orderId,则顺便回填 glt_user_ticket.order_id/order_no,减少后续同步/查询依赖兜底分支。
|
||||||
|
if (resolvedByOrderGoodsId && userTicket.getOrderId() == null && shopOrderId != null) {
|
||||||
|
if (!StringUtils.hasText(shopOrderNo)) {
|
||||||
|
ShopOrder order = shopOrderService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopOrder>()
|
||||||
|
.select(ShopOrder::getOrderNo)
|
||||||
|
.eq(ShopOrder::getTenantId, tenantId)
|
||||||
|
.eq(ShopOrder::getDeleted, 0)
|
||||||
|
.eq(ShopOrder::getOrderId, shopOrderId)
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
if (order != null) {
|
||||||
|
shopOrderNo = order.getOrderNo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaUpdateWrapper<GltUserTicket> backfill = new LambdaUpdateWrapper<GltUserTicket>()
|
||||||
|
.eq(GltUserTicket::getTenantId, tenantId)
|
||||||
|
.eq(GltUserTicket::getDeleted, 0)
|
||||||
|
.eq(GltUserTicket::getId, userTicket.getId());
|
||||||
|
backfill.set(GltUserTicket::getOrderId, shopOrderId);
|
||||||
|
if (!StringUtils.hasText(userTicket.getOrderNo()) && StringUtils.hasText(shopOrderNo)) {
|
||||||
|
backfill.set(GltUserTicket::getOrderNo, shopOrderNo);
|
||||||
|
}
|
||||||
|
backfill.set(GltUserTicket::getUpdateTime, now);
|
||||||
|
try {
|
||||||
|
gltUserTicketService.update(backfill);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("回填水票关联商城订单信息失败(不影响主流程) - tenantId={}, userTicketId={}, orderId={}, orderNo={}",
|
||||||
|
tenantId, userTicket.getId(), shopOrderId, shopOrderNo, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LambdaUpdateWrapper<ShopOrder> uw = new LambdaUpdateWrapper<ShopOrder>()
|
||||||
|
.eq(ShopOrder::getTenantId, tenantId)
|
||||||
|
.eq(ShopOrder::getDeleted, 0)
|
||||||
|
.and(w -> w.ne(ShopOrder::getOrderStatus, 1).or().isNull(ShopOrder::getOrderStatus))
|
||||||
|
.set(ShopOrder::getOrderStatus, 1)
|
||||||
|
.set(ShopOrder::getUpdateTime, now);
|
||||||
|
if (shopOrderId != null) {
|
||||||
|
uw.eq(ShopOrder::getOrderId, shopOrderId);
|
||||||
|
} else {
|
||||||
|
uw.eq(ShopOrder::getOrderNo, shopOrderNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean updated = shopOrderService.update(uw);
|
||||||
|
if (updated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 幂等:若已是 1,则视为成功;否则记录日志便于排查关联关系/数据缺失
|
||||||
|
LambdaQueryWrapper<ShopOrder> qw = new LambdaQueryWrapper<ShopOrder>()
|
||||||
|
.eq(ShopOrder::getTenantId, tenantId)
|
||||||
|
.eq(ShopOrder::getDeleted, 0)
|
||||||
|
.eq(ShopOrder::getOrderStatus, 1);
|
||||||
|
if (shopOrderId != null) {
|
||||||
|
qw.eq(ShopOrder::getOrderId, shopOrderId);
|
||||||
|
} else {
|
||||||
|
qw.eq(ShopOrder::getOrderNo, shopOrderNo);
|
||||||
|
}
|
||||||
|
if (shopOrderService.count(qw) <= 0) {
|
||||||
|
log.warn("送水订单完成但同步商城订单完成状态失败 - tenantId={}, ticketOrderId={}, shopOrderId={}, shopOrderNo={}",
|
||||||
|
tenantId, ticketOrderId, shopOrderId, shopOrderNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void settleRiderCommissionIfEligible(Integer ticketOrderId, Integer tenantId, boolean requirePhoto) {
|
||||||
|
if (ticketOrderId == null || tenantId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 目前仅租户10584启用该提成规则,避免影响其他租户历史逻辑。
|
||||||
|
if (tenantId != TENANT_ID_10584) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionTemplate.executeWithoutResult(status -> {
|
||||||
|
// 锁定送水订单行:避免并发下重复结算(如:配送员送达&用户确认收货同时触发)
|
||||||
|
GltTicketOrder order = this.lambdaQuery()
|
||||||
|
.eq(GltTicketOrder::getId, ticketOrderId)
|
||||||
|
.eq(GltTicketOrder::getTenantId, tenantId)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.last("limit 1 for update")
|
||||||
|
.one();
|
||||||
|
if (order == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer riderId = order.getRiderId();
|
||||||
|
if (riderId == null || riderId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer deliveryStatus = order.getDeliveryStatus();
|
||||||
|
if (requirePhoto) {
|
||||||
|
// 配送员拍照上传触发:至少需要到“待客户确认”或“已完成”状态,且存在送达照片。
|
||||||
|
if (deliveryStatus == null || (deliveryStatus != DELIVERY_STATUS_WAIT_CONFIRM && deliveryStatus != DELIVERY_STATUS_FINISHED)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(order.getSendEndImg())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 用户确认收货触发:必须为“已完成”状态。
|
||||||
|
if (deliveryStatus == null || deliveryStatus != DELIVERY_STATUS_FINISHED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int qty = order.getTotalNum() == null ? 0 : order.getTotalNum();
|
||||||
|
if (qty <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal money = RIDER_UNIT_COMMISSION
|
||||||
|
.multiply(BigDecimal.valueOf(qty))
|
||||||
|
.setScale(RIDER_COMMISSION_SCALE, RoundingMode.HALF_UP);
|
||||||
|
if (money.signum() <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String orderNo = "gltTicketOrder:" + order.getId();
|
||||||
|
String comments = "配送员提成(ticketOrderId=" + order.getId() + ",unit=" + RIDER_UNIT_COMMISSION + ",qty=" + qty + ")";
|
||||||
|
|
||||||
|
// 幂等:同一送水订单同一配送员只结算一次
|
||||||
|
boolean already = shopDealerCapitalService.count(
|
||||||
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
|
.eq(ShopDealerCapital::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerCapital::getFlowType, 10)
|
||||||
|
.eq(ShopDealerCapital::getUserId, riderId)
|
||||||
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
|
.likeRight(ShopDealerCapital::getComments, "配送员提成(ticketOrderId=" + order.getId() + ",")
|
||||||
|
) > 0;
|
||||||
|
if (already) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 送水订单提成:先入冻结金额 freeze_money(与分销订单佣金一致)
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
boolean updated = shopDealerUserService.update(
|
||||||
|
new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ShopDealerUser>()
|
||||||
|
.eq(ShopDealerUser::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerUser::getUserId, riderId)
|
||||||
|
.setSql("freeze_money = IFNULL(freeze_money,0) + " + money.toPlainString())
|
||||||
|
.setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString())
|
||||||
|
.set(ShopDealerUser::getUpdateTime, now)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
// 配送员可能未开通分销账户:创建后再尝试入账一次(与分销结算逻辑保持一致)
|
||||||
|
ShopDealerUser existed = shopDealerUserService.getOne(
|
||||||
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ShopDealerUser>()
|
||||||
|
.eq(ShopDealerUser::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerUser::getUserId, riderId)
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
if (existed == null) {
|
||||||
|
ShopDealerUser newDealerUser = new ShopDealerUser();
|
||||||
|
newDealerUser.setTenantId(tenantId);
|
||||||
|
newDealerUser.setUserId(riderId);
|
||||||
|
newDealerUser.setType(0);
|
||||||
|
newDealerUser.setIsDelete(0);
|
||||||
|
newDealerUser.setSortNumber(0);
|
||||||
|
newDealerUser.setFirstNum(0);
|
||||||
|
newDealerUser.setSecondNum(0);
|
||||||
|
newDealerUser.setThirdNum(0);
|
||||||
|
newDealerUser.setMoney(BigDecimal.ZERO);
|
||||||
|
newDealerUser.setFreezeMoney(BigDecimal.ZERO);
|
||||||
|
newDealerUser.setTotalMoney(BigDecimal.ZERO);
|
||||||
|
try {
|
||||||
|
User sysUser = userMapper.selectByIdIgnoreTenant(riderId);
|
||||||
|
if (sysUser != null) {
|
||||||
|
newDealerUser.setRealName(sysUser.getRealName() != null ? sysUser.getRealName() : sysUser.getNickname());
|
||||||
|
newDealerUser.setMobile(sysUser.getPhone());
|
||||||
|
}
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
// 基础信息补齐失败不影响入账
|
||||||
|
}
|
||||||
|
newDealerUser.setCreateTime(now);
|
||||||
|
newDealerUser.setUpdateTime(now);
|
||||||
|
shopDealerUserService.save(newDealerUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
updated = shopDealerUserService.update(
|
||||||
|
new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ShopDealerUser>()
|
||||||
|
.eq(ShopDealerUser::getTenantId, tenantId)
|
||||||
|
.eq(ShopDealerUser::getUserId, riderId)
|
||||||
|
.setSql("freeze_money = IFNULL(freeze_money,0) + " + money.toPlainString())
|
||||||
|
.setSql("total_money = IFNULL(total_money,0) + " + money.toPlainString())
|
||||||
|
.set(ShopDealerUser::getUpdateTime, now)
|
||||||
|
);
|
||||||
|
if (!updated) {
|
||||||
|
log.warn("配送员提成入账失败:未找到/创建分销账户 - tenantId={}, ticketOrderId={}, riderId={}", tenantId, order.getId(), riderId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ShopDealerCapital cap = new ShopDealerCapital();
|
||||||
|
cap.setUserId(riderId);
|
||||||
|
cap.setOrderNo(orderNo);
|
||||||
|
cap.setFlowType(10);
|
||||||
|
cap.setMoney(money);
|
||||||
|
cap.setComments(comments);
|
||||||
|
cap.setToUserId(order.getUserId());
|
||||||
|
cap.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
|
||||||
|
cap.setTenantId(tenantId);
|
||||||
|
cap.setCreateTime(now);
|
||||||
|
cap.setUpdateTime(now);
|
||||||
|
shopDealerCapitalService.save(cap);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package com.gxwebsoft.glt.service.impl;
|
||||||
|
|
||||||
|
import com.gxwebsoft.glt.entity.GltUserTicket;
|
||||||
|
import com.gxwebsoft.glt.entity.GltUserTicketLog;
|
||||||
|
import com.gxwebsoft.glt.entity.GltUserTicketRelease;
|
||||||
|
import com.gxwebsoft.glt.mapper.GltUserTicketLogMapper;
|
||||||
|
import com.gxwebsoft.glt.mapper.GltUserTicketMapper;
|
||||||
|
import com.gxwebsoft.glt.mapper.GltUserTicketReleaseMapper;
|
||||||
|
import com.gxwebsoft.glt.service.GltUserTicketAutoReleaseService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 冻结水票自动释放实现:
|
||||||
|
* - 读取到期且待释放的 release 记录(FOR UPDATE 加锁,防止重复处理)
|
||||||
|
* - 释放成功:更新 user_ticket 数量 & 将 release.status 置为 1,并写入流水
|
||||||
|
* - 释放失败:将 release.status 置为 2(数据异常/冻结不足等)
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GltUserTicketAutoReleaseServiceImpl implements GltUserTicketAutoReleaseService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变更类型:冻结释放
|
||||||
|
* <p>
|
||||||
|
* 现有发放类型为 10(见 GltTicketIssueService.CHANGE_TYPE_ISSUE),这里取 11。
|
||||||
|
*/
|
||||||
|
private static final int CHANGE_TYPE_RELEASE = 11;
|
||||||
|
|
||||||
|
/** release.status:待释放 */
|
||||||
|
private static final int RELEASE_STATUS_PENDING = 0;
|
||||||
|
/** release.status:已释放 */
|
||||||
|
private static final int RELEASE_STATUS_DONE = 1;
|
||||||
|
/** release.status:释放失败(数据异常/冻结不足等,需人工处理) */
|
||||||
|
private static final int RELEASE_STATUS_FAILED = 2;
|
||||||
|
|
||||||
|
private final GltUserTicketReleaseMapper releaseMapper;
|
||||||
|
private final GltUserTicketMapper userTicketMapper;
|
||||||
|
private final GltUserTicketLogMapper userTicketLogMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public int releaseDue(LocalDateTime now, int batchSize) {
|
||||||
|
int limit = Math.max(batchSize, 1);
|
||||||
|
List<GltUserTicketRelease> dueList = releaseMapper.selectDueForUpdate(now, limit);
|
||||||
|
if (dueList == null || dueList.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int success = 0;
|
||||||
|
for (GltUserTicketRelease rel : dueList) {
|
||||||
|
if (rel.getId() == null || rel.getStatus() == null || rel.getStatus() != RELEASE_STATUS_PENDING) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer qtyObj = rel.getReleaseQty();
|
||||||
|
if (qtyObj == null || qtyObj <= 0) {
|
||||||
|
markFailed(rel.getId(), now, "releaseQty无效");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int qty = qtyObj;
|
||||||
|
|
||||||
|
if (rel.getUserTicketId() == null || rel.getUserId() == null || rel.getTenantId() == null) {
|
||||||
|
markFailed(rel.getId(), now, "缺少userTicketId/userId/tenantId");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long userTicketIdLong = rel.getUserTicketId();
|
||||||
|
if (userTicketIdLong > Integer.MAX_VALUE || userTicketIdLong < 1) {
|
||||||
|
markFailed(rel.getId(), now, "userTicketId超范围");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Integer userTicketId = (int) userTicketIdLong;
|
||||||
|
|
||||||
|
// 先释放冻结数量(条件更新,确保 frozen_qty >= qty)
|
||||||
|
int updated = userTicketMapper.releaseFrozenQty(
|
||||||
|
userTicketId,
|
||||||
|
rel.getUserId(),
|
||||||
|
rel.getTenantId(),
|
||||||
|
qty,
|
||||||
|
now
|
||||||
|
);
|
||||||
|
if (updated <= 0) {
|
||||||
|
markFailed(rel.getId(), now, "冻结不足/水票不存在/状态不符");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入流水(可用 +qty,冻结 -qty)
|
||||||
|
GltUserTicket ticket = userTicketMapper.selectById(userTicketId);
|
||||||
|
GltUserTicketLog logRow = new GltUserTicketLog();
|
||||||
|
logRow.setUserTicketId(userTicketId);
|
||||||
|
logRow.setChangeType(CHANGE_TYPE_RELEASE);
|
||||||
|
logRow.setChangeAvailable(qty);
|
||||||
|
logRow.setChangeFrozen(-qty);
|
||||||
|
logRow.setChangeUsed(0);
|
||||||
|
if (ticket != null) {
|
||||||
|
logRow.setAvailableAfter(ticket.getAvailableQty());
|
||||||
|
logRow.setFrozenAfter(ticket.getFrozenQty());
|
||||||
|
logRow.setUsedAfter(ticket.getUsedQty());
|
||||||
|
}
|
||||||
|
logRow.setOrderId(null);
|
||||||
|
logRow.setOrderNo(null);
|
||||||
|
logRow.setUserId(rel.getUserId());
|
||||||
|
logRow.setSortNumber(0);
|
||||||
|
logRow.setComments("冻结水票到期释放");
|
||||||
|
logRow.setStatus(0);
|
||||||
|
logRow.setDeleted(0);
|
||||||
|
logRow.setTenantId(rel.getTenantId());
|
||||||
|
logRow.setCreateTime(now);
|
||||||
|
logRow.setUpdateTime(now);
|
||||||
|
userTicketLogMapper.insert(logRow);
|
||||||
|
|
||||||
|
// 标记释放记录已完成(放在最后:若流水失败则回滚)
|
||||||
|
releaseMapper.updateStatus(rel.getId(), RELEASE_STATUS_DONE, now);
|
||||||
|
|
||||||
|
success++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markFailed(Long releaseId, LocalDateTime now, String reason) {
|
||||||
|
releaseMapper.updateStatus(releaseId, RELEASE_STATUS_FAILED, now);
|
||||||
|
log.warn("冻结水票释放标记失败 - releaseId={}, reason={}", releaseId, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,417 @@
|
|||||||
|
package com.gxwebsoft.glt.task;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.gxwebsoft.common.core.annotation.IgnoreTenant;
|
||||||
|
import com.gxwebsoft.glt.entity.GltTicketOrder;
|
||||||
|
import com.gxwebsoft.glt.entity.GltTicketTemplate;
|
||||||
|
import com.gxwebsoft.glt.entity.GltUserTicket;
|
||||||
|
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
||||||
|
import com.gxwebsoft.glt.service.GltTicketTemplateService;
|
||||||
|
import com.gxwebsoft.glt.service.GltUserTicketService;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopDealerCapital;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopDealerOrder;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopDealerUser;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopOrder;
|
||||||
|
import com.gxwebsoft.shop.service.ShopDealerCapitalService;
|
||||||
|
import com.gxwebsoft.shop.service.ShopDealerOrderService;
|
||||||
|
import com.gxwebsoft.shop.service.ShopDealerUserService;
|
||||||
|
import com.gxwebsoft.shop.service.ShopOrderService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户10584:分销佣金解冻任务
|
||||||
|
*
|
||||||
|
* <p>规则:</p>
|
||||||
|
* <p>1) 送水套餐(formId in 水票模板 goodsId):订单号关联的水票产生了第一次送水订单,且该第一次送水订单状态=已完成(40) -> 解冻。</p>
|
||||||
|
* <p>2) 非送水套餐(formId not in 水票模板 goodsId):订单已确认收货(orderStatus=1) -> 解冻。</p>
|
||||||
|
*
|
||||||
|
* <p>实现策略:以 ShopDealerCapital(flowType=10) 的“佣金明细”为解冻粒度,
|
||||||
|
* 每条佣金明细对应生成一条 ShopDealerCapital(flowType=50) 作为幂等标记,并执行
|
||||||
|
* ShopDealerUser.freezeMoney -> ShopDealerUser.money 的转移。</p>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@ConditionalOnProperty(prefix = "dealer.commission.unfreeze10584", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||||
|
public class DealerCommissionUnfreeze10584Task {
|
||||||
|
|
||||||
|
private static final int TENANT_ID = 10584;
|
||||||
|
|
||||||
|
private static final int ORDER_STATUS_CONFIRMED_RECEIVE = 1;
|
||||||
|
|
||||||
|
private static final int MAX_ELIGIBLE_ORDER_NOS_PER_RUN = 200;
|
||||||
|
private static final int MAX_ELIGIBLE_TICKET_ORDERS_PER_RUN = 200;
|
||||||
|
private static final int MAX_CAPITALS_PER_RUN = 500;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private TransactionTemplate transactionTemplate;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ShopOrderService shopOrderService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ShopDealerCapitalService shopDealerCapitalService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ShopDealerOrderService shopDealerOrderService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ShopDealerUserService shopDealerUserService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GltUserTicketService gltUserTicketService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GltTicketOrderService gltTicketOrderService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GltTicketTemplateService gltTicketTemplateService;
|
||||||
|
|
||||||
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
@Scheduled(cron = "${dealer.commission.unfreeze10584.cron:0/30 * * * * ?}")
|
||||||
|
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
|
||||||
|
public void run() {
|
||||||
|
if (!running.compareAndSet(false, true)) {
|
||||||
|
log.warn("分销佣金解冻任务仍在执行中,本轮跳过 - tenantId={}", TENANT_ID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Set<Integer> waterFormIds = loadWaterFormIds();
|
||||||
|
if (waterFormIds.isEmpty()) {
|
||||||
|
// 送水/非送水的判断依赖模板 goodsId;拿不到会导致误判,宁可跳过本轮。
|
||||||
|
log.warn("分销佣金解冻任务跳过:未找到水票模板 goodsId - tenantId={}", TENANT_ID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先按“最近确认收货”的订单扫描,避免总是卡在很早的历史订单上。
|
||||||
|
Set<String> eligibleOrderNos = new HashSet<>();
|
||||||
|
eligibleOrderNos.addAll(findEligibleNonWaterOrderNos(waterFormIds, true));
|
||||||
|
eligibleOrderNos.addAll(findEligibleWaterOrderNosByFirstFinishedTicketOrder(waterFormIds));
|
||||||
|
|
||||||
|
if (eligibleOrderNos.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ShopDealerCapital> capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos);
|
||||||
|
if (capitals.isEmpty()) {
|
||||||
|
// 若本轮没有取到佣金明细,回退再按“最早确认收货”的订单扫一轮,尽量覆盖历史遗留未解冻。
|
||||||
|
eligibleOrderNos.clear();
|
||||||
|
eligibleOrderNos.addAll(findEligibleNonWaterOrderNos(waterFormIds, false));
|
||||||
|
eligibleOrderNos.addAll(findEligibleWaterOrderNosByFirstFinishedTicketOrder(waterFormIds));
|
||||||
|
capitals = findCapitalsByEligibleOrderNos(eligibleOrderNos);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (capitals.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int unfrozen = 0;
|
||||||
|
for (ShopDealerCapital cap : capitals) {
|
||||||
|
try {
|
||||||
|
boolean ok = unfreezeOneCapitalIfNeeded(cap);
|
||||||
|
if (ok) {
|
||||||
|
unfrozen++;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解冻佣金失败,将在下次任务重试 - tenantId={}, capitalId={}, orderNo={}, userId={}",
|
||||||
|
TENANT_ID, cap != null ? cap.getId() : null, cap != null ? cap.getOrderNo() : null, cap != null ? cap.getUserId() : null, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unfrozen > 0) {
|
||||||
|
log.info("分销佣金解冻完成 - tenantId={}, eligibleOrderNos={}, scannedCapitals={}, unfrozen={}",
|
||||||
|
TENANT_ID, eligibleOrderNos.size(), capitals.size(), unfrozen);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
running.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Integer> loadWaterFormIds() {
|
||||||
|
return gltTicketTemplateService.list(
|
||||||
|
new LambdaQueryWrapper<GltTicketTemplate>()
|
||||||
|
.eq(GltTicketTemplate::getTenantId, TENANT_ID)
|
||||||
|
.eq(GltTicketTemplate::getDeleted, 0)
|
||||||
|
.isNotNull(GltTicketTemplate::getGoodsId)
|
||||||
|
).stream()
|
||||||
|
.map(GltTicketTemplate::getGoodsId)
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> findEligibleNonWaterOrderNos(Set<Integer> waterFormIds, boolean newestFirst) {
|
||||||
|
LambdaQueryWrapper<ShopOrder> qw = new LambdaQueryWrapper<ShopOrder>()
|
||||||
|
.eq(ShopOrder::getTenantId, TENANT_ID)
|
||||||
|
.eq(ShopOrder::getDeleted, 0)
|
||||||
|
.eq(ShopOrder::getPayStatus, true)
|
||||||
|
.eq(ShopOrder::getOrderStatus, ORDER_STATUS_CONFIRMED_RECEIVE)
|
||||||
|
.and(w -> w.notIn(ShopOrder::getFormId, waterFormIds).or().isNull(ShopOrder::getFormId))
|
||||||
|
.isNotNull(ShopOrder::getOrderNo);
|
||||||
|
|
||||||
|
if (newestFirst) {
|
||||||
|
qw.orderByDesc(ShopOrder::getUpdateTime).orderByDesc(ShopOrder::getOrderId);
|
||||||
|
} else {
|
||||||
|
qw.orderByAsc(ShopOrder::getUpdateTime).orderByAsc(ShopOrder::getOrderId);
|
||||||
|
}
|
||||||
|
qw.last("limit " + MAX_ELIGIBLE_ORDER_NOS_PER_RUN);
|
||||||
|
|
||||||
|
return shopOrderService.list(qw).stream()
|
||||||
|
.map(ShopOrder::getOrderNo)
|
||||||
|
.filter(s -> s != null && !s.isBlank())
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> findEligibleWaterOrderNosByFirstFinishedTicketOrder(Set<Integer> waterFormIds) {
|
||||||
|
// 缓存减少 DB 往返:userTicketId -> 是否“第一次送水单已完成”
|
||||||
|
Map<Integer, Boolean> firstFinishedCache = new HashMap<>();
|
||||||
|
Map<Integer, String> userTicketOrderNoCache = new HashMap<>();
|
||||||
|
|
||||||
|
List<GltTicketOrder> finishedTicketOrders = gltTicketOrderService.list(
|
||||||
|
new LambdaQueryWrapper<GltTicketOrder>()
|
||||||
|
.eq(GltTicketOrder::getTenantId, TENANT_ID)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.eq(GltTicketOrder::getDeliveryStatus, GltTicketOrderService.DELIVERY_STATUS_FINISHED)
|
||||||
|
.isNotNull(GltTicketOrder::getUserTicketId)
|
||||||
|
.orderByDesc(GltTicketOrder::getId)
|
||||||
|
.last("limit " + MAX_ELIGIBLE_TICKET_ORDERS_PER_RUN)
|
||||||
|
);
|
||||||
|
|
||||||
|
Set<String> orderNos = new HashSet<>();
|
||||||
|
for (GltTicketOrder ticketOrder : finishedTicketOrders) {
|
||||||
|
Integer userTicketId = ticketOrder.getUserTicketId();
|
||||||
|
if (userTicketId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean firstFinished = firstFinishedCache.computeIfAbsent(userTicketId, id -> isFirstTicketOrderFinished(id));
|
||||||
|
if (!firstFinished) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String orderNo = userTicketOrderNoCache.computeIfAbsent(userTicketId, id -> findOrderNoByUserTicketId(id));
|
||||||
|
if (orderNo == null || orderNo.isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再校验一次确实是送水套餐订单,避免误关联
|
||||||
|
ShopOrder shopOrder = shopOrderService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopOrder>()
|
||||||
|
.eq(ShopOrder::getTenantId, TENANT_ID)
|
||||||
|
.eq(ShopOrder::getDeleted, 0)
|
||||||
|
.eq(ShopOrder::getPayStatus, true)
|
||||||
|
.eq(ShopOrder::getOrderNo, orderNo)
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
if (shopOrder == null || shopOrder.getFormId() == null || !waterFormIds.contains(shopOrder.getFormId())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
orderNos.add(orderNo);
|
||||||
|
if (orderNos.size() >= MAX_ELIGIBLE_ORDER_NOS_PER_RUN) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return orderNos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isFirstTicketOrderFinished(Integer userTicketId) {
|
||||||
|
if (userTicketId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
GltTicketOrder first = gltTicketOrderService.getOne(
|
||||||
|
new LambdaQueryWrapper<GltTicketOrder>()
|
||||||
|
.eq(GltTicketOrder::getTenantId, TENANT_ID)
|
||||||
|
.eq(GltTicketOrder::getDeleted, 0)
|
||||||
|
.eq(GltTicketOrder::getUserTicketId, userTicketId)
|
||||||
|
.orderByAsc(GltTicketOrder::getId)
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
return first != null && first.getDeliveryStatus() != null && first.getDeliveryStatus() == GltTicketOrderService.DELIVERY_STATUS_FINISHED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String findOrderNoByUserTicketId(Integer userTicketId) {
|
||||||
|
if (userTicketId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
GltUserTicket userTicket = gltUserTicketService.getOne(
|
||||||
|
new LambdaQueryWrapper<GltUserTicket>()
|
||||||
|
.eq(GltUserTicket::getTenantId, TENANT_ID)
|
||||||
|
.eq(GltUserTicket::getDeleted, 0)
|
||||||
|
.eq(GltUserTicket::getId, userTicketId)
|
||||||
|
.last("limit 1")
|
||||||
|
);
|
||||||
|
return userTicket != null ? userTicket.getOrderNo() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ShopDealerCapital> findCapitalsByEligibleOrderNos(Set<String> eligibleOrderNos) {
|
||||||
|
if (eligibleOrderNos == null || eligibleOrderNos.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return shopDealerCapitalService.list(
|
||||||
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
|
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
|
||||||
|
.eq(ShopDealerCapital::getFlowType, 10)
|
||||||
|
.in(ShopDealerCapital::getOrderNo, eligibleOrderNos)
|
||||||
|
.isNotNull(ShopDealerCapital::getOrderNo)
|
||||||
|
.orderByAsc(ShopDealerCapital::getId)
|
||||||
|
.last("limit " + MAX_CAPITALS_PER_RUN)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean unfreezeOneCapitalIfNeeded(ShopDealerCapital cap) {
|
||||||
|
if (cap == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Integer capitalId = cap.getId();
|
||||||
|
Integer dealerUserId = cap.getUserId();
|
||||||
|
String orderNo = cap.getOrderNo();
|
||||||
|
BigDecimal amount = cap.getMoney();
|
||||||
|
if (capitalId == null || dealerUserId == null || orderNo == null || orderNo.isBlank() || amount == null || amount.signum() <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String markerComment = buildUnfreezeMarkerComment(capitalId);
|
||||||
|
|
||||||
|
// 快速幂等检查(避免每条都进事务)
|
||||||
|
boolean already = shopDealerCapitalService.count(
|
||||||
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
|
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
|
||||||
|
.eq(ShopDealerCapital::getFlowType, 50)
|
||||||
|
.eq(ShopDealerCapital::getUserId, dealerUserId)
|
||||||
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
|
.eq(ShopDealerCapital::getComments, markerComment)
|
||||||
|
) > 0;
|
||||||
|
if (already) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean.TRUE.equals(transactionTemplate.execute(status -> {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
// 锁定分销商账户行,避免多实例并发导致同一条佣金重复解冻。
|
||||||
|
ShopDealerUser dealerUser = shopDealerUserService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopDealerUser>()
|
||||||
|
.eq(ShopDealerUser::getTenantId, TENANT_ID)
|
||||||
|
.eq(ShopDealerUser::getUserId, dealerUserId)
|
||||||
|
.last("limit 1 for update")
|
||||||
|
);
|
||||||
|
if (dealerUser == null) {
|
||||||
|
log.warn("解冻失败:未找到分销账户 - tenantId={}, orderNo={}, dealerUserId={}, amount={}",
|
||||||
|
TENANT_ID, orderNo, dealerUserId, amount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RR 隔离级别下,先锁 user 行,再用锁定读检查 marker,避免“读到旧快照”导致重复解冻。
|
||||||
|
ShopDealerCapital existedMarker = shopDealerCapitalService.getOne(
|
||||||
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
|
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
|
||||||
|
.eq(ShopDealerCapital::getFlowType, 50)
|
||||||
|
.eq(ShopDealerCapital::getUserId, dealerUserId)
|
||||||
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
|
.eq(ShopDealerCapital::getComments, markerComment)
|
||||||
|
.last("limit 1 for update")
|
||||||
|
);
|
||||||
|
if (existedMarker != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal freezeMoney = dealerUser.getFreezeMoney() != null ? dealerUser.getFreezeMoney() : BigDecimal.ZERO;
|
||||||
|
if (freezeMoney.compareTo(amount) < 0) {
|
||||||
|
log.warn("解冻失败:冻结金额不足 - tenantId={}, orderNo={}, dealerUserId={}, freezeMoney={}, amount={}",
|
||||||
|
TENANT_ID, orderNo, dealerUserId, freezeMoney, amount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal moneyVal = dealerUser.getMoney() != null ? dealerUser.getMoney() : BigDecimal.ZERO;
|
||||||
|
dealerUser.setFreezeMoney(freezeMoney.subtract(amount));
|
||||||
|
dealerUser.setMoney(moneyVal.add(amount));
|
||||||
|
dealerUser.setUpdateTime(now);
|
||||||
|
if (!shopDealerUserService.updateById(dealerUser)) {
|
||||||
|
log.warn("解冻失败:更新分销账户失败 - tenantId={}, orderNo={}, dealerUserId={}, amount={}",
|
||||||
|
TENANT_ID, orderNo, dealerUserId, amount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShopDealerCapital marker = new ShopDealerCapital();
|
||||||
|
marker.setUserId(dealerUserId);
|
||||||
|
marker.setOrderNo(orderNo);
|
||||||
|
marker.setFlowType(50);
|
||||||
|
marker.setMoney(amount);
|
||||||
|
marker.setComments(markerComment);
|
||||||
|
marker.setToUserId(cap.getToUserId());
|
||||||
|
marker.setMonth(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM")));
|
||||||
|
marker.setTenantId(TENANT_ID);
|
||||||
|
marker.setCreateTime(now);
|
||||||
|
marker.setUpdateTime(now);
|
||||||
|
shopDealerCapitalService.save(marker);
|
||||||
|
|
||||||
|
// 佣金全部解冻完成后,将分销订单状态置为“已解冻”(0)。
|
||||||
|
// 以当前任务生成的 flowType=50 marker 数量作为完成度判断,避免提前将订单置为已解冻。
|
||||||
|
setDealerOrderUnfrozenIfCompleted(orderNo, now);
|
||||||
|
|
||||||
|
log.info("佣金解冻成功 - tenantId={}, orderNo={}, dealerUserId={}, amount={}, capitalId={}",
|
||||||
|
TENANT_ID, orderNo, dealerUserId, amount, capitalId);
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDealerOrderUnfrozenIfCompleted(String orderNo, LocalDateTime now) {
|
||||||
|
if (orderNo == null || orderNo.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long totalCommissions = shopDealerCapitalService.count(
|
||||||
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
|
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
|
||||||
|
.eq(ShopDealerCapital::getFlowType, 10)
|
||||||
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
|
);
|
||||||
|
if (totalCommissions <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long unfrozenMarkers = shopDealerCapitalService.count(
|
||||||
|
new LambdaQueryWrapper<ShopDealerCapital>()
|
||||||
|
.eq(ShopDealerCapital::getTenantId, TENANT_ID)
|
||||||
|
.eq(ShopDealerCapital::getFlowType, 50)
|
||||||
|
.eq(ShopDealerCapital::getOrderNo, orderNo)
|
||||||
|
.like(ShopDealerCapital::getComments, "佣金解冻(capitalId=")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (unfrozenMarkers < totalCommissions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean updated = shopDealerOrderService.update(
|
||||||
|
new LambdaUpdateWrapper<ShopDealerOrder>()
|
||||||
|
.eq(ShopDealerOrder::getTenantId, TENANT_ID)
|
||||||
|
.eq(ShopDealerOrder::getOrderNo, orderNo)
|
||||||
|
.set(ShopDealerOrder::getIsUnfreeze, 1)
|
||||||
|
.set(ShopDealerOrder::getUnfreezeTime, now)
|
||||||
|
.set(ShopDealerOrder::getUpdateTime, now)
|
||||||
|
);
|
||||||
|
if (!updated) {
|
||||||
|
log.warn("已完成佣金解冻,但更新分销订单isUnfreeze失败/无记录 - tenantId={}, orderNo={}", TENANT_ID, orderNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildUnfreezeMarkerComment(Integer capitalId) {
|
||||||
|
return "佣金解冻(capitalId=" + capitalId + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.gxwebsoft.glt.task;
|
||||||
|
|
||||||
|
import com.gxwebsoft.common.core.annotation.IgnoreTenant;
|
||||||
|
import com.gxwebsoft.glt.service.GltTicketOrderService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户10584:送水订单超时自动确认收货任务
|
||||||
|
*
|
||||||
|
* <p>扫描已送达待确认(30)且送达时间超过24小时的订单,自动置为已完成(40)。</p>
|
||||||
|
* <p>自动确认后会触发配送员提成结算(幂等)。</p>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(prefix = "glt.ticket-order.auto-confirm10584", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||||
|
public class GltTicketOrderAutoConfirm10584Task {
|
||||||
|
|
||||||
|
private static final int TENANT_ID = 10584;
|
||||||
|
private static final int TIMEOUT_HOURS = 24;
|
||||||
|
|
||||||
|
private final GltTicketOrderService gltTicketOrderService;
|
||||||
|
|
||||||
|
@Value("${glt.ticket-order.auto-confirm10584.batch-size:200}")
|
||||||
|
private int batchSize;
|
||||||
|
|
||||||
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
@Scheduled(cron = "${glt.ticket-order.auto-confirm10584.cron:0/33 * * * * ?}")
|
||||||
|
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;内部使用 tenantId=10584 精确过滤")
|
||||||
|
public void run() {
|
||||||
|
if (!running.compareAndSet(false, true)) {
|
||||||
|
log.warn("送水订单超时自动确认任务仍在执行中,本轮跳过 - tenantId={}", TENANT_ID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
int confirmed = gltTicketOrderService.autoConfirmTimeout(TENANT_ID, now, TIMEOUT_HOURS, Math.max(batchSize, 1));
|
||||||
|
if (confirmed > 0) {
|
||||||
|
log.info("送水订单超时自动确认完成 - tenantId={}, confirmed={}, now={}", TENANT_ID, confirmed, now);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
running.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.gxwebsoft.glt.task;
|
||||||
|
|
||||||
|
import com.gxwebsoft.common.core.annotation.IgnoreTenant;
|
||||||
|
import com.gxwebsoft.glt.service.GltUserTicketAutoReleaseService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 冻结水票自动释放任务:
|
||||||
|
* - 扫描 glt_user_ticket_release 中到期且待释放(status=0)的记录
|
||||||
|
* - 释放成功:frozen -> available,并将 release.status 置为 1
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnProperty(prefix = "glt.ticket.auto-release", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||||
|
public class GltUserTicketAutoReleaseTask {
|
||||||
|
|
||||||
|
private final GltUserTicketAutoReleaseService autoReleaseService;
|
||||||
|
|
||||||
|
@Value("${glt.ticket.auto-release.batch-size:200}")
|
||||||
|
private int batchSize;
|
||||||
|
|
||||||
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
@Scheduled(cron = "${glt.ticket.auto-release.cron:0 */10 * * * ?}")
|
||||||
|
@IgnoreTenant("定时任务无登录态,需忽略租户隔离;释放记录自带 tenantId,更新时会校验 tenantId")
|
||||||
|
public void run() {
|
||||||
|
if (!running.compareAndSet(false, true)) {
|
||||||
|
log.warn("冻结水票自动释放任务仍在执行中,本轮跳过");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
int released = autoReleaseService.releaseDue(now, Math.max(batchSize, 1));
|
||||||
|
if (released > 0) {
|
||||||
|
log.info("冻结水票自动释放完成 - released={}, now={}", released, now);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
running.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.gxwebsoft.shop.controller;
|
||||||
|
|
||||||
|
import com.gxwebsoft.common.core.web.BaseController;
|
||||||
|
import com.gxwebsoft.shop.service.ShopStoreFenceService;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopStoreFence;
|
||||||
|
import com.gxwebsoft.shop.param.ShopStoreFenceParam;
|
||||||
|
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;
|
||||||
|
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-07 18:10:25
|
||||||
|
*/
|
||||||
|
@Tag(name = "黄家明_电子围栏管理")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/shop/shop-store-fence")
|
||||||
|
public class ShopStoreFenceController extends BaseController {
|
||||||
|
@Resource
|
||||||
|
private ShopStoreFenceService shopStoreFenceService;
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreFence:list')")
|
||||||
|
@Operation(summary = "分页查询黄家明_电子围栏")
|
||||||
|
@GetMapping("/page")
|
||||||
|
public ApiResult<PageResult<ShopStoreFence>> page(ShopStoreFenceParam param) {
|
||||||
|
// 使用关联查询
|
||||||
|
return success(shopStoreFenceService.pageRel(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreFence:list')")
|
||||||
|
@Operation(summary = "查询全部黄家明_电子围栏")
|
||||||
|
@GetMapping()
|
||||||
|
public ApiResult<List<ShopStoreFence>> list(ShopStoreFenceParam param) {
|
||||||
|
// 使用关联查询
|
||||||
|
return success(shopStoreFenceService.listRel(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreFence:list')")
|
||||||
|
@Operation(summary = "根据id查询黄家明_电子围栏")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResult<ShopStoreFence> get(@PathVariable("id") Integer id) {
|
||||||
|
// 使用关联查询
|
||||||
|
return success(shopStoreFenceService.getByIdRel(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreFence:save')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "添加黄家明_电子围栏")
|
||||||
|
@PostMapping()
|
||||||
|
public ApiResult<?> save(@RequestBody ShopStoreFence shopStoreFence) {
|
||||||
|
if (shopStoreFenceService.save(shopStoreFence)) {
|
||||||
|
return success("添加成功");
|
||||||
|
}
|
||||||
|
return fail("添加失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreFence:update')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "修改黄家明_电子围栏")
|
||||||
|
@PutMapping()
|
||||||
|
public ApiResult<?> update(@RequestBody ShopStoreFence shopStoreFence) {
|
||||||
|
if (shopStoreFenceService.updateById(shopStoreFence)) {
|
||||||
|
return success("修改成功");
|
||||||
|
}
|
||||||
|
return fail("修改失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreFence:remove')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "删除黄家明_电子围栏")
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ApiResult<?> remove(@PathVariable("id") Integer id) {
|
||||||
|
if (shopStoreFenceService.removeById(id)) {
|
||||||
|
return success("删除成功");
|
||||||
|
}
|
||||||
|
return fail("删除失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreFence:save')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "批量添加黄家明_电子围栏")
|
||||||
|
@PostMapping("/batch")
|
||||||
|
public ApiResult<?> saveBatch(@RequestBody List<ShopStoreFence> list) {
|
||||||
|
if (shopStoreFenceService.saveBatch(list)) {
|
||||||
|
return success("添加成功");
|
||||||
|
}
|
||||||
|
return fail("添加失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreFence:update')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "批量修改黄家明_电子围栏")
|
||||||
|
@PutMapping("/batch")
|
||||||
|
public ApiResult<?> removeBatch(@RequestBody BatchParam<ShopStoreFence> batchParam) {
|
||||||
|
if (batchParam.update(shopStoreFenceService, "id")) {
|
||||||
|
return success("修改成功");
|
||||||
|
}
|
||||||
|
return fail("修改失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreFence:remove')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "批量删除黄家明_电子围栏")
|
||||||
|
@DeleteMapping("/batch")
|
||||||
|
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
|
||||||
|
if (shopStoreFenceService.removeByIds(ids)) {
|
||||||
|
return success("删除成功");
|
||||||
|
}
|
||||||
|
return fail("删除失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package com.gxwebsoft.shop.controller;
|
||||||
|
|
||||||
|
import com.gxwebsoft.common.core.web.BaseController;
|
||||||
|
import com.gxwebsoft.common.system.entity.User;
|
||||||
|
import com.gxwebsoft.shop.service.ShopStoreWarehouseService;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopStoreWarehouse;
|
||||||
|
import com.gxwebsoft.shop.param.ShopStoreWarehouseParam;
|
||||||
|
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;
|
||||||
|
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-07 18:10:25
|
||||||
|
*/
|
||||||
|
@Tag(name = "仓库管理")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/shop/shop-store-warehouse")
|
||||||
|
public class ShopStoreWarehouseController extends BaseController {
|
||||||
|
@Resource
|
||||||
|
private ShopStoreWarehouseService shopStoreWarehouseService;
|
||||||
|
|
||||||
|
@Operation(summary = "分页查询仓库")
|
||||||
|
@GetMapping("/page")
|
||||||
|
public ApiResult<PageResult<ShopStoreWarehouse>> page(ShopStoreWarehouseParam param) {
|
||||||
|
// 使用关联查询
|
||||||
|
return success(shopStoreWarehouseService.pageRel(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "查询全部仓库")
|
||||||
|
@GetMapping()
|
||||||
|
public ApiResult<List<ShopStoreWarehouse>> list(ShopStoreWarehouseParam param) {
|
||||||
|
// 使用关联查询
|
||||||
|
return success(shopStoreWarehouseService.listRel(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "根据id查询仓库")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ApiResult<ShopStoreWarehouse> get(@PathVariable("id") Integer id) {
|
||||||
|
// 使用关联查询
|
||||||
|
return success(shopStoreWarehouseService.getByIdRel(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreWarehouse:save')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "添加仓库")
|
||||||
|
@PostMapping()
|
||||||
|
public ApiResult<?> save(@RequestBody ShopStoreWarehouse shopStoreWarehouse) {
|
||||||
|
// 记录当前登录用户id
|
||||||
|
User loginUser = getLoginUser();
|
||||||
|
if (loginUser != null) {
|
||||||
|
shopStoreWarehouse.setUserId(loginUser.getUserId());
|
||||||
|
}
|
||||||
|
if (shopStoreWarehouseService.save(shopStoreWarehouse)) {
|
||||||
|
return success("添加成功");
|
||||||
|
}
|
||||||
|
return fail("添加失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreWarehouse:update')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "修改仓库")
|
||||||
|
@PutMapping()
|
||||||
|
public ApiResult<?> update(@RequestBody ShopStoreWarehouse shopStoreWarehouse) {
|
||||||
|
if (shopStoreWarehouseService.updateById(shopStoreWarehouse)) {
|
||||||
|
return success("修改成功");
|
||||||
|
}
|
||||||
|
return fail("修改失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreWarehouse:remove')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "删除仓库")
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ApiResult<?> remove(@PathVariable("id") Integer id) {
|
||||||
|
if (shopStoreWarehouseService.removeById(id)) {
|
||||||
|
return success("删除成功");
|
||||||
|
}
|
||||||
|
return fail("删除失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreWarehouse:save')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "批量添加仓库")
|
||||||
|
@PostMapping("/batch")
|
||||||
|
public ApiResult<?> saveBatch(@RequestBody List<ShopStoreWarehouse> list) {
|
||||||
|
if (shopStoreWarehouseService.saveBatch(list)) {
|
||||||
|
return success("添加成功");
|
||||||
|
}
|
||||||
|
return fail("添加失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreWarehouse:update')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "批量修改仓库")
|
||||||
|
@PutMapping("/batch")
|
||||||
|
public ApiResult<?> removeBatch(@RequestBody BatchParam<ShopStoreWarehouse> batchParam) {
|
||||||
|
if (batchParam.update(shopStoreWarehouseService, "id")) {
|
||||||
|
return success("修改成功");
|
||||||
|
}
|
||||||
|
return fail("修改失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('shop:shopStoreWarehouse:remove')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "批量删除仓库")
|
||||||
|
@DeleteMapping("/batch")
|
||||||
|
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
|
||||||
|
if (shopStoreWarehouseService.removeByIds(ids)) {
|
||||||
|
return success("删除成功");
|
||||||
|
}
|
||||||
|
return fail("删除失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.gxwebsoft.shop.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信支付回调别名入口(兼容历史 notify_url)
|
||||||
|
*
|
||||||
|
* 说明:
|
||||||
|
* - 旧代码曾使用 /api/system/wx-pay/notify/{tenantId} 或 /api/shop/wx-pay/notify/{tenantId}
|
||||||
|
* - 微信支付“请求重入”要求 notify_url 等参数与首次一致,因此需要保留旧回调地址可用
|
||||||
|
*/
|
||||||
|
@Tag(name = "微信支付回调(别名)")
|
||||||
|
@RestController
|
||||||
|
public class WxPayNotifyAliasController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ShopOrderController shopOrderController;
|
||||||
|
|
||||||
|
@Operation(summary = "微信支付回调别名(兼容旧回调地址)")
|
||||||
|
@PostMapping({"/api/system/wx-pay/notify/{tenantId}", "/api/shop/wx-pay/notify/{tenantId}"})
|
||||||
|
public String wxNotifyAlias(@RequestHeader Map<String, String> header,
|
||||||
|
@RequestBody String body,
|
||||||
|
@PathVariable("tenantId") Integer tenantId) {
|
||||||
|
// ShopOrderController.wxNotify 读取的是小写 header key,这里做一次兼容转换
|
||||||
|
Map<String, String> lower = new HashMap<>();
|
||||||
|
if (header != null) {
|
||||||
|
header.forEach((k, v) -> {
|
||||||
|
if (k != null) {
|
||||||
|
lower.put(k.toLowerCase(Locale.ROOT), v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return shopOrderController.wxNotify(lower, body, tenantId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
69
src/main/java/com/gxwebsoft/shop/entity/ShopStoreFence.java
Normal file
69
src/main/java/com/gxwebsoft/shop/entity/ShopStoreFence.java
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package com.gxwebsoft.shop.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 黄家明_电子围栏
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-02-07 18:10:25
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Schema(name = "ShopStoreFence对象", description = "黄家明_电子围栏")
|
||||||
|
public class ShopStoreFence implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "自增ID")
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Schema(description = "围栏名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "类型 0圆形 1方形")
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
@Schema(description = "定位")
|
||||||
|
private String location;
|
||||||
|
|
||||||
|
@Schema(description = "经度")
|
||||||
|
private String longitude;
|
||||||
|
|
||||||
|
@Schema(description = "纬度")
|
||||||
|
private String latitude;
|
||||||
|
|
||||||
|
@Schema(description = "区域")
|
||||||
|
private String district;
|
||||||
|
|
||||||
|
@Schema(description = "电子围栏轮廓")
|
||||||
|
private String points;
|
||||||
|
|
||||||
|
@Schema(description = "排序(数字越小越靠前)")
|
||||||
|
private Integer sortNumber;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String comments;
|
||||||
|
|
||||||
|
@Schema(description = "状态, 0正常, 1冻结")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@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,81 @@
|
|||||||
|
package com.gxwebsoft.shop.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仓库
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-02-07 18:10:25
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@Schema(name = "ShopStoreWarehouse对象", description = "仓库")
|
||||||
|
public class ShopStoreWarehouse implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "自增ID")
|
||||||
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Schema(description = "仓库名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "唯一标识")
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@Schema(description = "类型 中心仓,区域仓,门店仓")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@Schema(description = "仓库地址")
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
@Schema(description = "真实姓名")
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
@Schema(description = "联系电话")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Schema(description = "所在省份")
|
||||||
|
private String province;
|
||||||
|
|
||||||
|
@Schema(description = "所在城市")
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
@Schema(description = "所在辖区")
|
||||||
|
private String region;
|
||||||
|
|
||||||
|
@Schema(description = "经纬度")
|
||||||
|
private String lngAndLat;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String comments;
|
||||||
|
|
||||||
|
@Schema(description = "排序号")
|
||||||
|
private Integer sortNumber;
|
||||||
|
|
||||||
|
@Schema(description = "是否删除")
|
||||||
|
private Integer isDelete;
|
||||||
|
|
||||||
|
@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.shop.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopStoreFence;
|
||||||
|
import com.gxwebsoft.shop.param.ShopStoreFenceParam;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 黄家明_电子围栏Mapper
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-02-07 18:10:25
|
||||||
|
*/
|
||||||
|
public interface ShopStoreFenceMapper extends BaseMapper<ShopStoreFence> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询
|
||||||
|
*
|
||||||
|
* @param page 分页对象
|
||||||
|
* @param param 查询参数
|
||||||
|
* @return List<ShopStoreFence>
|
||||||
|
*/
|
||||||
|
List<ShopStoreFence> selectPageRel(@Param("page") IPage<ShopStoreFence> page,
|
||||||
|
@Param("param") ShopStoreFenceParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询全部
|
||||||
|
*
|
||||||
|
* @param param 查询参数
|
||||||
|
* @return List<User>
|
||||||
|
*/
|
||||||
|
List<ShopStoreFence> selectListRel(@Param("param") ShopStoreFenceParam param);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.gxwebsoft.shop.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopStoreWarehouse;
|
||||||
|
import com.gxwebsoft.shop.param.ShopStoreWarehouseParam;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仓库Mapper
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-02-07 18:10:25
|
||||||
|
*/
|
||||||
|
public interface ShopStoreWarehouseMapper extends BaseMapper<ShopStoreWarehouse> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询
|
||||||
|
*
|
||||||
|
* @param page 分页对象
|
||||||
|
* @param param 查询参数
|
||||||
|
* @return List<ShopStoreWarehouse>
|
||||||
|
*/
|
||||||
|
List<ShopStoreWarehouse> selectPageRel(@Param("page") IPage<ShopStoreWarehouse> page,
|
||||||
|
@Param("param") ShopStoreWarehouseParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询全部
|
||||||
|
*
|
||||||
|
* @param param 查询参数
|
||||||
|
* @return List<User>
|
||||||
|
*/
|
||||||
|
List<ShopStoreWarehouse> selectListRel(@Param("param") ShopStoreWarehouseParam param);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?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.shop.mapper.ShopStoreFenceMapper">
|
||||||
|
|
||||||
|
<!-- 关联查询sql -->
|
||||||
|
<sql id="selectSql">
|
||||||
|
SELECT a.*
|
||||||
|
FROM shop_store_fence a
|
||||||
|
<where>
|
||||||
|
<if test="param.id != null">
|
||||||
|
AND a.id = #{param.id}
|
||||||
|
</if>
|
||||||
|
<if test="param.name != null">
|
||||||
|
AND a.name LIKE CONCAT('%', #{param.name}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.type != null">
|
||||||
|
AND a.type = #{param.type}
|
||||||
|
</if>
|
||||||
|
<if test="param.location != null">
|
||||||
|
AND a.location LIKE CONCAT('%', #{param.location}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.longitude != null">
|
||||||
|
AND a.longitude LIKE CONCAT('%', #{param.longitude}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.latitude != null">
|
||||||
|
AND a.latitude LIKE CONCAT('%', #{param.latitude}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.district != null">
|
||||||
|
AND a.district LIKE CONCAT('%', #{param.district}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.points != null">
|
||||||
|
AND a.points LIKE CONCAT('%', #{param.points}, '%')
|
||||||
|
</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.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.shop.entity.ShopStoreFence">
|
||||||
|
<include refid="selectSql"></include>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查询全部 -->
|
||||||
|
<select id="selectListRel" resultType="com.gxwebsoft.shop.entity.ShopStoreFence">
|
||||||
|
<include refid="selectSql"></include>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?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.shop.mapper.ShopStoreWarehouseMapper">
|
||||||
|
|
||||||
|
<!-- 关联查询sql -->
|
||||||
|
<sql id="selectSql">
|
||||||
|
SELECT a.*
|
||||||
|
FROM shop_store_warehouse a
|
||||||
|
<where>
|
||||||
|
<if test="param.id != null">
|
||||||
|
AND a.id = #{param.id}
|
||||||
|
</if>
|
||||||
|
<if test="param.name != null">
|
||||||
|
AND a.name LIKE CONCAT('%', #{param.name}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.code != null">
|
||||||
|
AND a.code LIKE CONCAT('%', #{param.code}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.type != null">
|
||||||
|
AND a.type LIKE CONCAT('%', #{param.type}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.address != null">
|
||||||
|
AND a.address LIKE CONCAT('%', #{param.address}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.realName != null">
|
||||||
|
AND a.real_name LIKE CONCAT('%', #{param.realName}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.phone != null">
|
||||||
|
AND a.phone LIKE CONCAT('%', #{param.phone}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.province != null">
|
||||||
|
AND a.province LIKE CONCAT('%', #{param.province}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.city != null">
|
||||||
|
AND a.city LIKE CONCAT('%', #{param.city}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.region != null">
|
||||||
|
AND a.region LIKE CONCAT('%', #{param.region}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.lngAndLat != null">
|
||||||
|
AND a.lng_and_lat LIKE CONCAT('%', #{param.lngAndLat}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.userId != null">
|
||||||
|
AND a.user_id = #{param.userId}
|
||||||
|
</if>
|
||||||
|
<if test="param.comments != null">
|
||||||
|
AND a.comments LIKE CONCAT('%', #{param.comments}, '%')
|
||||||
|
</if>
|
||||||
|
<if test="param.sortNumber != null">
|
||||||
|
AND a.sort_number = #{param.sortNumber}
|
||||||
|
</if>
|
||||||
|
<if test="param.isDelete != null">
|
||||||
|
AND a.is_delete = #{param.isDelete}
|
||||||
|
</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.shop.entity.ShopStoreWarehouse">
|
||||||
|
<include refid="selectSql"></include>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 查询全部 -->
|
||||||
|
<select id="selectListRel" resultType="com.gxwebsoft.shop.entity.ShopStoreWarehouse">
|
||||||
|
<include refid="selectSql"></include>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.gxwebsoft.shop.param;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import com.gxwebsoft.common.core.annotation.QueryField;
|
||||||
|
import com.gxwebsoft.common.core.annotation.QueryType;
|
||||||
|
import com.gxwebsoft.common.core.web.BaseParam;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 黄家明_电子围栏查询参数
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-02-07 18:10:24
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
@Schema(name = "ShopStoreFenceParam对象", description = "黄家明_电子围栏查询参数")
|
||||||
|
public class ShopStoreFenceParam extends BaseParam {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "自增ID")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Schema(description = "围栏名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "类型 0圆形 1方形")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer type;
|
||||||
|
|
||||||
|
@Schema(description = "定位")
|
||||||
|
private String location;
|
||||||
|
|
||||||
|
@Schema(description = "经度")
|
||||||
|
private String longitude;
|
||||||
|
|
||||||
|
@Schema(description = "纬度")
|
||||||
|
private String latitude;
|
||||||
|
|
||||||
|
@Schema(description = "区域")
|
||||||
|
private String district;
|
||||||
|
|
||||||
|
@Schema(description = "电子围栏轮廓")
|
||||||
|
private String points;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.gxwebsoft.shop.param;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import com.gxwebsoft.common.core.annotation.QueryField;
|
||||||
|
import com.gxwebsoft.common.core.annotation.QueryType;
|
||||||
|
import com.gxwebsoft.common.core.web.BaseParam;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仓库查询参数
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-02-07 18:10:25
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
@Schema(name = "ShopStoreWarehouseParam对象", description = "仓库查询参数")
|
||||||
|
public class ShopStoreWarehouseParam extends BaseParam {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "自增ID")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@Schema(description = "仓库名称")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Schema(description = "唯一标识")
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@Schema(description = "类型 中心仓,区域仓,门店仓")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@Schema(description = "仓库地址")
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
@Schema(description = "真实姓名")
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
@Schema(description = "联系电话")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Schema(description = "所在省份")
|
||||||
|
private String province;
|
||||||
|
|
||||||
|
@Schema(description = "所在城市")
|
||||||
|
private String city;
|
||||||
|
|
||||||
|
@Schema(description = "所在辖区")
|
||||||
|
private String region;
|
||||||
|
|
||||||
|
@Schema(description = "经纬度")
|
||||||
|
private String lngAndLat;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String comments;
|
||||||
|
|
||||||
|
@Schema(description = "排序号")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer sortNumber;
|
||||||
|
|
||||||
|
@Schema(description = "是否删除")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer isDelete;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.gxwebsoft.shop.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopStoreFence;
|
||||||
|
import com.gxwebsoft.shop.param.ShopStoreFenceParam;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 黄家明_电子围栏Service
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-02-07 18:10:25
|
||||||
|
*/
|
||||||
|
public interface ShopStoreFenceService extends IService<ShopStoreFence> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页关联查询
|
||||||
|
*
|
||||||
|
* @param param 查询参数
|
||||||
|
* @return PageResult<ShopStoreFence>
|
||||||
|
*/
|
||||||
|
PageResult<ShopStoreFence> pageRel(ShopStoreFenceParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联查询全部
|
||||||
|
*
|
||||||
|
* @param param 查询参数
|
||||||
|
* @return List<ShopStoreFence>
|
||||||
|
*/
|
||||||
|
List<ShopStoreFence> listRel(ShopStoreFenceParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询
|
||||||
|
*
|
||||||
|
* @param id 自增ID
|
||||||
|
* @return ShopStoreFence
|
||||||
|
*/
|
||||||
|
ShopStoreFence getByIdRel(Integer id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前租户是否配置了任一启用围栏(status=0)。
|
||||||
|
*/
|
||||||
|
boolean hasEnabledFences(Integer tenantId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验坐标是否落在任一启用围栏内。
|
||||||
|
* <p>
|
||||||
|
* 约定:
|
||||||
|
* - 围栏按 tenantId + status=0 过滤;
|
||||||
|
* - 支持多个围栏:命中任意一个即通过;
|
||||||
|
* - 无围栏配置:直接放行;
|
||||||
|
* - 围栏 points 异常:抛出异常(避免误送)。
|
||||||
|
*
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
* @param lng 经度
|
||||||
|
* @param lat 纬度
|
||||||
|
*/
|
||||||
|
void validatePointInEnabledFences(Integer tenantId, double lng, double lat);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.gxwebsoft.shop.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopStoreWarehouse;
|
||||||
|
import com.gxwebsoft.shop.param.ShopStoreWarehouseParam;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仓库Service
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-02-07 18:10:25
|
||||||
|
*/
|
||||||
|
public interface ShopStoreWarehouseService extends IService<ShopStoreWarehouse> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页关联查询
|
||||||
|
*
|
||||||
|
* @param param 查询参数
|
||||||
|
* @return PageResult<ShopStoreWarehouse>
|
||||||
|
*/
|
||||||
|
PageResult<ShopStoreWarehouse> pageRel(ShopStoreWarehouseParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联查询全部
|
||||||
|
*
|
||||||
|
* @param param 查询参数
|
||||||
|
* @return List<ShopStoreWarehouse>
|
||||||
|
*/
|
||||||
|
List<ShopStoreWarehouse> listRel(ShopStoreWarehouseParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据id查询
|
||||||
|
*
|
||||||
|
* @param id 自增ID
|
||||||
|
* @return ShopStoreWarehouse
|
||||||
|
*/
|
||||||
|
ShopStoreWarehouse getByIdRel(Integer id);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package com.gxwebsoft.shop.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||||
|
import com.gxwebsoft.shop.mapper.ShopStoreFenceMapper;
|
||||||
|
import com.gxwebsoft.shop.service.ShopStoreFenceService;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopStoreFence;
|
||||||
|
import com.gxwebsoft.shop.param.ShopStoreFenceParam;
|
||||||
|
import com.gxwebsoft.common.core.web.PageParam;
|
||||||
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
|
import com.gxwebsoft.shop.util.GeoFenceUtil;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 黄家明_电子围栏Service实现
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-02-07 18:10:25
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ShopStoreFenceServiceImpl extends ServiceImpl<ShopStoreFenceMapper, ShopStoreFence> implements ShopStoreFenceService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ShopStoreFenceServiceImpl.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<ShopStoreFence> pageRel(ShopStoreFenceParam param) {
|
||||||
|
PageParam<ShopStoreFence, ShopStoreFenceParam> page = new PageParam<>(param);
|
||||||
|
page.setDefaultOrder("sort_number asc, create_time desc");
|
||||||
|
List<ShopStoreFence> list = baseMapper.selectPageRel(page, param);
|
||||||
|
return new PageResult<>(list, page.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ShopStoreFence> listRel(ShopStoreFenceParam param) {
|
||||||
|
List<ShopStoreFence> list = baseMapper.selectListRel(param);
|
||||||
|
// 排序
|
||||||
|
PageParam<ShopStoreFence, ShopStoreFenceParam> page = new PageParam<>();
|
||||||
|
page.setDefaultOrder("sort_number asc, create_time desc");
|
||||||
|
return page.sortRecords(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ShopStoreFence getByIdRel(Integer id) {
|
||||||
|
ShopStoreFenceParam param = new ShopStoreFenceParam();
|
||||||
|
param.setId(id);
|
||||||
|
return param.getOne(baseMapper.selectListRel(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasEnabledFences(Integer tenantId) {
|
||||||
|
if (tenantId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.count(new LambdaQueryWrapper<ShopStoreFence>()
|
||||||
|
.eq(ShopStoreFence::getTenantId, tenantId)
|
||||||
|
.eq(ShopStoreFence::getStatus, 0)) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void validatePointInEnabledFences(Integer tenantId, double lng, double lat) {
|
||||||
|
if (tenantId == null) {
|
||||||
|
// tenantId 缺失时不做围栏校验,避免误伤;上层应保证 tenantId 正确传入
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ShopStoreFence> fences = this.list(new LambdaQueryWrapper<ShopStoreFence>()
|
||||||
|
.eq(ShopStoreFence::getTenantId, tenantId)
|
||||||
|
.eq(ShopStoreFence::getStatus, 0)
|
||||||
|
.orderByAsc(ShopStoreFence::getSortNumber)
|
||||||
|
.orderByDesc(ShopStoreFence::getCreateTime));
|
||||||
|
|
||||||
|
// 无围栏配置:默认放行
|
||||||
|
if (fences == null || fences.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ShopStoreFence fence : fences) {
|
||||||
|
if (fence == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<GeoFenceUtil.Point> polygon;
|
||||||
|
try {
|
||||||
|
polygon = GeoFenceUtil.parsePolygonPoints(fence.getPoints());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// points 异常:直接拒单并记录日志,避免误送
|
||||||
|
log.error("围栏 points 解析失败,tenantId={}, fenceId={}, points={}",
|
||||||
|
tenantId, fence.getId(), fence.getPoints(), e);
|
||||||
|
throw new BusinessException("配送范围配置异常,请联系商家");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (polygon == null || polygon.size() < 3) {
|
||||||
|
log.error("围栏 points 点数不足,tenantId={}, fenceId={}, points={}",
|
||||||
|
tenantId, fence.getId(), fence.getPoints());
|
||||||
|
throw new BusinessException("配送范围配置异常,请联系商家");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GeoFenceUtil.containsInclusive(polygon, lng, lat)) {
|
||||||
|
return; // 命中任一围栏即通过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BusinessException("收货地址不在配送范围内");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.gxwebsoft.shop.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.gxwebsoft.shop.mapper.ShopStoreWarehouseMapper;
|
||||||
|
import com.gxwebsoft.shop.service.ShopStoreWarehouseService;
|
||||||
|
import com.gxwebsoft.shop.entity.ShopStoreWarehouse;
|
||||||
|
import com.gxwebsoft.shop.param.ShopStoreWarehouseParam;
|
||||||
|
import com.gxwebsoft.common.core.web.PageParam;
|
||||||
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仓库Service实现
|
||||||
|
*
|
||||||
|
* @author 科技小王子
|
||||||
|
* @since 2026-02-07 18:10:25
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ShopStoreWarehouseServiceImpl extends ServiceImpl<ShopStoreWarehouseMapper, ShopStoreWarehouse> implements ShopStoreWarehouseService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<ShopStoreWarehouse> pageRel(ShopStoreWarehouseParam param) {
|
||||||
|
PageParam<ShopStoreWarehouse, ShopStoreWarehouseParam> page = new PageParam<>(param);
|
||||||
|
page.setDefaultOrder("sort_number asc, create_time desc");
|
||||||
|
List<ShopStoreWarehouse> list = baseMapper.selectPageRel(page, param);
|
||||||
|
return new PageResult<>(list, page.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ShopStoreWarehouse> listRel(ShopStoreWarehouseParam param) {
|
||||||
|
List<ShopStoreWarehouse> list = baseMapper.selectListRel(param);
|
||||||
|
// 排序
|
||||||
|
PageParam<ShopStoreWarehouse, ShopStoreWarehouseParam> page = new PageParam<>();
|
||||||
|
page.setDefaultOrder("sort_number asc, create_time desc");
|
||||||
|
return page.sortRecords(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ShopStoreWarehouse getByIdRel(Integer id) {
|
||||||
|
ShopStoreWarehouseParam param = new ShopStoreWarehouseParam();
|
||||||
|
param.setId(id);
|
||||||
|
return param.getOne(baseMapper.selectListRel(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 "订单商品";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
224
src/main/java/com/gxwebsoft/shop/util/GeoFenceUtil.java
Normal file
224
src/main/java/com/gxwebsoft/shop/util/GeoFenceUtil.java
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
package com.gxwebsoft.shop.util;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 电子围栏(多边形)相关工具:
|
||||||
|
* - points 字符串解析(兼容常见格式:JSON数组 / "lng,lat;lng,lat" / 纯数字逗号序列)
|
||||||
|
* - 点在多边形内判断(边界视为“在围栏内”)
|
||||||
|
*/
|
||||||
|
public final class GeoFenceUtil {
|
||||||
|
|
||||||
|
private GeoFenceUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Point {
|
||||||
|
public final double lng;
|
||||||
|
public final double lat;
|
||||||
|
|
||||||
|
public Point(double lng, double lat) {
|
||||||
|
this.lng = lng;
|
||||||
|
this.lat = lat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final ObjectMapper OM = new ObjectMapper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析围栏 points 字段为多边形点列表。
|
||||||
|
* <p>
|
||||||
|
* 支持格式示例:
|
||||||
|
* - [[lng,lat],[lng,lat],...]
|
||||||
|
* - [{"lng":..,"lat":..}, ...] / [{"longitude":..,"latitude":..}, ...]
|
||||||
|
* - "lng,lat;lng,lat;..."
|
||||||
|
* - "lng,lat|lng,lat|..."
|
||||||
|
* - "lng,lat,lng,lat,lng,lat,..."(纯逗号序列,偶数个数字)
|
||||||
|
*/
|
||||||
|
public static List<Point> parsePolygonPoints(String pointsRaw) {
|
||||||
|
if (pointsRaw == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
String s = pointsRaw.trim();
|
||||||
|
if (s.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON array
|
||||||
|
if (s.startsWith("[")) {
|
||||||
|
try {
|
||||||
|
JsonNode root = OM.readTree(s);
|
||||||
|
if (!root.isArray()) {
|
||||||
|
throw new IllegalArgumentException("points 不是数组");
|
||||||
|
}
|
||||||
|
List<Point> pts = new ArrayList<>();
|
||||||
|
for (JsonNode node : root) {
|
||||||
|
if (node == null || node.isNull()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (node.isArray() && node.size() >= 2) {
|
||||||
|
double a = node.get(0).asDouble();
|
||||||
|
double b = node.get(1).asDouble();
|
||||||
|
pts.add(normalizeLngLat(a, b));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (node.isObject()) {
|
||||||
|
JsonNode lngNode = node.get("lng");
|
||||||
|
if (lngNode == null) lngNode = node.get("longitude");
|
||||||
|
JsonNode latNode = node.get("lat");
|
||||||
|
if (latNode == null) latNode = node.get("latitude");
|
||||||
|
if (lngNode == null || latNode == null) {
|
||||||
|
throw new IllegalArgumentException("points 对象缺少 lng/lat 字段");
|
||||||
|
}
|
||||||
|
pts.add(normalizeLngLat(lngNode.asDouble(), latNode.asDouble()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("points 数组元素格式不支持");
|
||||||
|
}
|
||||||
|
return trimClosingPoint(pts);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("points JSON 解析失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非 JSON:尝试按分隔符拆分
|
||||||
|
String normalized = s.replace("\n", "").replace("\r", "").trim();
|
||||||
|
String[] pairTokens;
|
||||||
|
if (normalized.contains(";")) {
|
||||||
|
pairTokens = normalized.split(";+");
|
||||||
|
return trimClosingPoint(parsePairs(pairTokens));
|
||||||
|
}
|
||||||
|
if (normalized.contains("|")) {
|
||||||
|
pairTokens = normalized.split("\\|+");
|
||||||
|
return trimClosingPoint(parsePairs(pairTokens));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 纯逗号序列:lng,lat,lng,lat,...
|
||||||
|
if (normalized.contains(",")) {
|
||||||
|
String[] nums = normalized.split(",+");
|
||||||
|
List<Double> ds = new ArrayList<>();
|
||||||
|
for (String num : nums) {
|
||||||
|
String t = num.trim();
|
||||||
|
if (t.isEmpty()) continue;
|
||||||
|
ds.add(Double.parseDouble(t));
|
||||||
|
}
|
||||||
|
if (ds.size() % 2 != 0) {
|
||||||
|
throw new IllegalArgumentException("points 逗号序列数字个数必须为偶数");
|
||||||
|
}
|
||||||
|
List<Point> pts = new ArrayList<>();
|
||||||
|
for (int i = 0; i < ds.size(); i += 2) {
|
||||||
|
pts.add(normalizeLngLat(ds.get(i), ds.get(i + 1)));
|
||||||
|
}
|
||||||
|
return trimClosingPoint(pts);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("points 格式不支持");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Point> parsePairs(String[] pairTokens) {
|
||||||
|
List<Point> pts = new ArrayList<>();
|
||||||
|
for (String token : pairTokens) {
|
||||||
|
if (token == null) continue;
|
||||||
|
String t = token.trim();
|
||||||
|
if (t.isEmpty()) continue;
|
||||||
|
String[] parts = t.split("[,\\s]+");
|
||||||
|
if (parts.length < 2) {
|
||||||
|
throw new IllegalArgumentException("points 点格式错误: " + t);
|
||||||
|
}
|
||||||
|
double a = Double.parseDouble(parts[0].trim());
|
||||||
|
double b = Double.parseDouble(parts[1].trim());
|
||||||
|
pts.add(normalizeLngLat(a, b));
|
||||||
|
}
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归一化输入的两个数为 (lng,lat)。
|
||||||
|
* 若可明显判断是 (lat,lng) 则自动交换(仅在一个维度超出经纬常规范围时触发)。
|
||||||
|
*/
|
||||||
|
private static Point normalizeLngLat(double first, double second) {
|
||||||
|
boolean firstLooksLat = Math.abs(first) <= 90;
|
||||||
|
boolean secondLooksLat = Math.abs(second) <= 90;
|
||||||
|
boolean firstLooksLng = Math.abs(first) <= 180;
|
||||||
|
boolean secondLooksLng = Math.abs(second) <= 180;
|
||||||
|
|
||||||
|
// 明显是 (lat,lng):first 像纬度且 second 像经度,但 second 不像纬度
|
||||||
|
if (firstLooksLat && secondLooksLng && !secondLooksLat) {
|
||||||
|
return new Point(second, first);
|
||||||
|
}
|
||||||
|
// 默认按 (lng,lat)
|
||||||
|
if (!firstLooksLng || !secondLooksLat) {
|
||||||
|
// 仍返回默认顺序,让上层决定是否拒单(避免隐式容错导致误判)
|
||||||
|
return new Point(first, second);
|
||||||
|
}
|
||||||
|
return new Point(first, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Point> trimClosingPoint(List<Point> pts) {
|
||||||
|
if (pts == null || pts.size() < 2) {
|
||||||
|
return pts == null ? Collections.emptyList() : pts;
|
||||||
|
}
|
||||||
|
Point first = pts.get(0);
|
||||||
|
Point last = pts.get(pts.size() - 1);
|
||||||
|
if (nearlyEqual(first.lng, last.lng, 1e-12) && nearlyEqual(first.lat, last.lat, 1e-12)) {
|
||||||
|
return new ArrayList<>(pts.subList(0, pts.size() - 1));
|
||||||
|
}
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点在多边形内判断(ray casting),边界视为 inside。
|
||||||
|
*/
|
||||||
|
public static boolean containsInclusive(List<Point> polygon, double lng, double lat) {
|
||||||
|
if (polygon == null || polygon.size() < 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final double eps = 1e-12;
|
||||||
|
boolean inside = false;
|
||||||
|
int n = polygon.size();
|
||||||
|
for (int i = 0, j = n - 1; i < n; j = i++) {
|
||||||
|
Point pi = polygon.get(i);
|
||||||
|
Point pj = polygon.get(j);
|
||||||
|
|
||||||
|
// 边界:点在线段上
|
||||||
|
if (pointOnSegment(lng, lat, pj.lng, pj.lat, pi.lng, pi.lat, eps)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 射线与边相交:以 lat 为 y
|
||||||
|
boolean intersect = ((pi.lat > lat) != (pj.lat > lat))
|
||||||
|
&& (lng < (pj.lng - pi.lng) * (lat - pi.lat) / (pj.lat - pi.lat + 0.0) + pi.lng);
|
||||||
|
if (intersect) {
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean pointOnSegment(double px, double py,
|
||||||
|
double ax, double ay,
|
||||||
|
double bx, double by,
|
||||||
|
double eps) {
|
||||||
|
// 叉积为 0 表示共线
|
||||||
|
double cross = (px - ax) * (by - ay) - (py - ay) * (bx - ax);
|
||||||
|
if (Math.abs(cross) > eps) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 点积判断是否在线段范围内
|
||||||
|
double dot = (px - ax) * (bx - ax) + (py - ay) * (by - ay);
|
||||||
|
if (dot < -eps) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
double lenSq = (bx - ax) * (bx - ax) + (by - ay) * (by - ay);
|
||||||
|
return dot - lenSq <= eps;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean nearlyEqual(double a, double b, double eps) {
|
||||||
|
return Math.abs(a - b) <= eps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
83
src/main/resources/application-glt.yml
Normal file
83
src/main/resources/application-glt.yml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 生产环境配置
|
||||||
|
|
||||||
|
# 数据源配置
|
||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://1Panel-mysql-XsWW:3306/modules?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||||
|
username: modules
|
||||||
|
password: tYmmMGh5wpwXR3ae
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
|
druid:
|
||||||
|
remove-abandoned: true
|
||||||
|
|
||||||
|
# redis
|
||||||
|
redis:
|
||||||
|
database: 0
|
||||||
|
host: 1Panel-redis-GmNr
|
||||||
|
port: 6379
|
||||||
|
password: redis_t74P8C
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
logging:
|
||||||
|
file:
|
||||||
|
name: websoft-modules.log
|
||||||
|
level:
|
||||||
|
root: WARN
|
||||||
|
com.gxwebsoft: ERROR
|
||||||
|
com.baomidou.mybatisplus: ERROR
|
||||||
|
|
||||||
|
socketio:
|
||||||
|
host: 0.0.0.0 #IP地址
|
||||||
|
|
||||||
|
# MQTT配置
|
||||||
|
mqtt:
|
||||||
|
enabled: true # 启用MQTT服务
|
||||||
|
host: tcp://132.232.214.96:1883
|
||||||
|
username: swdev
|
||||||
|
password: Sw20250523
|
||||||
|
client-id-prefix: hjm_car_
|
||||||
|
topic: /SW_GPS/#
|
||||||
|
qos: 2
|
||||||
|
connection-timeout: 10
|
||||||
|
keep-alive-interval: 20
|
||||||
|
auto-reconnect: true
|
||||||
|
|
||||||
|
# 框架配置
|
||||||
|
config:
|
||||||
|
# 文件服务器
|
||||||
|
file-server: https://file-s209.shoplnk.cn
|
||||||
|
# 生产环境接口
|
||||||
|
server-url: https://glt-server.websoft.top/api
|
||||||
|
upload-path: /www/wwwroot/file.ws
|
||||||
|
|
||||||
|
# 阿里云OSS云存储
|
||||||
|
endpoint: https://oss-cn-shenzhen.aliyuncs.com
|
||||||
|
accessKeyId: LTAI4GKGZ9Z2Z8JZ77c3GNZP
|
||||||
|
accessKeySecret: BiDkpS7UXj72HWwDWaFZxiXjNFBNCM
|
||||||
|
bucketName: oss-gxwebsoft
|
||||||
|
bucketDomain: https://oss.wsdns.cn
|
||||||
|
aliyunDomain: https://oss-gxwebsoft.oss-cn-shenzhen.aliyuncs.com
|
||||||
|
|
||||||
|
# 生产环境证书配置
|
||||||
|
certificate:
|
||||||
|
load-mode: VOLUME # 生产环境从Docker挂载卷加载
|
||||||
|
cert-root-path: /www/wwwroot/file.ws
|
||||||
|
|
||||||
|
# 支付配置缓存
|
||||||
|
payment:
|
||||||
|
cache:
|
||||||
|
# 支付配置缓存键前缀,生产环境使用 Payment:1* 格式
|
||||||
|
key-prefix: "Payment:1"
|
||||||
|
# 缓存过期时间(小时)
|
||||||
|
expire-hours: 24
|
||||||
|
# 阿里云翻译配置
|
||||||
|
aliyun:
|
||||||
|
translate:
|
||||||
|
access-key-id: LTAI5tEsyhW4GCKbds1qsopg
|
||||||
|
access-key-secret: zltFlQrYVAoq2KMFDWgLa3GhkMNeyO
|
||||||
|
endpoint: mt.cn-hangzhou.aliyuncs.com
|
||||||
|
wechatpay:
|
||||||
|
transfer:
|
||||||
|
scene-id: 1005
|
||||||
|
scene-report-infos-json: '[{"info_type":"岗位类型","info_content":"配送员"},{"info_type":"报酬说明","info_content":"12月份配送费"}]'
|
||||||
Reference in New Issue
Block a user