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:
2026-02-13 02:21:28 +08:00
parent a1a3d78dd6
commit 6f1503e89f
32 changed files with 4067 additions and 0 deletions

View File

@@ -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("删除失败");
}
}

View 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;
}

View File

@@ -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);
}

View File

@@ -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 &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{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>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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_orderstoreId/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);
});
}
}

View File

@@ -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);
}
}

View File

@@ -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 + ")";
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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("删除失败");
}
}

View File

@@ -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("删除失败");
}
}

View File

@@ -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);
}
}

View 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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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 &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{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>

View File

@@ -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 &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{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>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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("收货地址不在配送范围内");
}
}

View File

@@ -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));
}
}

View File

@@ -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 "订单商品";
}
}
}

View 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;
}
}

View 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月份配送费"}]'