Merge pull request 'feat(mcp): 新增商城订单查询工具 mall.order.list/detail/logistics.query (REQ-20260909-0004)' (#6) from feat/REQ-20260909-0004-mall-order-tools into main

This commit was merged in pull request #6.
This commit is contained in:
2026-09-09 19:05:16 +08:00
16 changed files with 1769 additions and 1 deletions
@@ -197,6 +197,88 @@
---
## 7. mall.order.list.query
查询商城订单列表(分页),对应老系统 `POST /mallGoodsOrder/listByPage`。
- 分类:customer | 类型:读
- 数据来源:m1 商城库(`xiaoqu_mall_d`)`mall_goods_order` 表
- 行级权限(`PermissionService.applyOrderRowFilter`,移植老接口 `WrapperUtils.getSaleCompany` 语义):
- 总部(isagent=0):全部;
- 代理商(isagent=1):`sale_company_id IN (自己 + 直属下级 + warehouse_agent_auth 代管代理)`,有代管物业授权时再 `OR property_id IN (agent_manage_district_auth 代管物业)`;
- 销售部门非负责人:`property_id IN (自己创建的物业 + 代管代理名下物业)`,与上一条互斥。
**入参**:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| orderNo | string | 否 | 订单编号,模糊匹配 |
| agentId | number | 否 | 代理商 id,精确匹配 |
| propertyId | number | 否 | 物业 id,精确匹配 |
| orderType | number | 否 | 订单类型:0=代理 1=物业 |
| orderState | number | 否 | 订单状态(单选):0=待付款 1=待出库 2=待发货 3=已发货 4=已签收 5=已收货 6=已取消 7=已删除 |
| payState | number | 否 | 回款状态:0=待付款 1=已付款 2=付款中 |
| startOrderTime | string | 否 | 下单时间起,`yyyy-MM-dd` |
| endOrderTime | string | 否 | 下单时间止,`yyyy-MM-dd`(含当天) |
| page | number | 否 | 页码,从 1 开始,默认 1 |
| rows | number | 否 | 每页条数,默认 20,上限 100(超出截断为 100 并在 notice 提示) |
**出参 records 字段**(按 id 倒序):
| 字段 | 说明 |
|---|---|
| id / orderNo / orderTime | 订单 id / 编号 / 下单时间(`yyyy-MM-dd HH:mm:ss`) |
| orderType / agentName / propertyName | 订单类型 / 代理名称 / 物业名称 |
| saleCompany | 销售公司 |
| payMoney / paidMoney / unPayMoney | 应付 / 已付 / 未付金额 |
| orderState / payState / warehouseState | 订单状态 / 回款状态 / 出库状态(0=未出库 1=已出库 2=部分出库) |
| customerName | 客户名称:orderType=0 取代理金蝶名称,否则取物业金蝶名称(批量富化,同老接口) |
---
## 8. mall.order.detail.query
按订单号查询商城订单详情(订单 + 商品明细 + 收货地址一次返回,合并老系统三个接口)。
- 分类:customer | 类型:读
- 数据来源:m1 库 `mall_goods_order` + `mall_order_details` + `mall_shipping_address`
- 行级权限:与 `mall.order.list.query` 一致;订单不存在或无权查看时 `record=null` 并在 notice 说明(两者不区分,不泄露订单是否存在)。
**入参**:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| orderNo | string | 是 | 订单编号 |
**出参**:
- `record`:订单关键字段(金额类 payMoney/paidMoney/unPayMoney/totalMoney/goodsMoney/postageMoney/discountMoney,状态类 orderState/payState/orderPayState/warehouseState/orderInvoiceState,时间类 orderTime/payTime/receiptTime/signTime/createTime,及 accountsType/paymentType/goodsType/saleCompany/payNo/memo 等)+ 收货信息(reveiveName/reveiveNumber/province/city/countyseat/address,由 `address_id` 关联 `mall_shipping_address`)。
- `items`:商品明细列表,每行含 spuNo/spuName/skuNo/count/price/payMoney/payState/warehouseState。
---
## 9. mall.order.logistics.query
按订单号查询商城订单物流(发货信息 + 物流轨迹),对应老系统 `GET /mallGoodsOrder/getMallLogistics`。
- 分类:customer | 类型:读
- 数据来源:m1 库 `mall_order_logistics` + `mall_order_logistics_detai`
- 行级权限:与 `mall.order.list.query` 一致;订单不存在或无权查看时 `logistics=null` 并在 notice 说明(两者不区分)。
- **注意**:轨迹表 `logistics_order_no` 字段老系统写入时塞的是订单号而非快递单号,故按 orderNo 查询(照搬老系统语义)。物流轨迹为系统内业务节点手工写入的记录,非实时快递查询,可能不全(菜鸟定时同步已停用)。
**入参**:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| orderNo | string | 是 | 订单编号 |
**出参**:
- `logistics`(无物流记录时为 null):logisticsName(快递公司)/ logisticsOrderNo(快递单号)/ sendMode(0=快递 1=自送)/ state(0=已出单 1=已出库 2=已发货 3=已揽件 4=运输中 5=待取件 6=派送中 7=已签收)/ deliverName / deliverNumber / sendTime / receivingGoodsName / reveiveName / reveiveNumber / address。
- `tracks`:轨迹列表,按时间正序,每行含 memo(轨迹描述)/ state / picList / createTime。
---
## 附录:在线查看工具清单
```bash
+2 -1
View File
@@ -15,9 +15,10 @@ SELECT '运营', 'operation', 'dev 种子:运营作用域', 1, 0
WHERE NOT EXISTS (SELECT 1 FROM agent_scope WHERE code = 'operation');
-- 作用域-工具:sys.ping / agent.count / agent.list.query / property.list.query / agent.detail.query / property.detail.query
-- / mall.order.list.query / mall.order.detail.query / mall.order.logistics.query
INSERT INTO scope_tool (scope_id, tool_id, create_id)
SELECT s.id, t.id, 0 FROM agent_scope s JOIN tool t
WHERE s.code = 'operation' AND t.name IN ('sys.ping', 'agent.count', 'agent.list.query', 'property.list.query', 'agent.detail.query', 'property.detail.query')
WHERE s.code = 'operation' AND t.name IN ('sys.ping', 'agent.count', 'agent.list.query', 'property.list.query', 'agent.detail.query', 'property.detail.query', 'mall.order.list.query', 'mall.order.detail.query', 'mall.order.logistics.query')
AND NOT EXISTS (SELECT 1 FROM scope_tool st WHERE st.scope_id = s.id AND st.tool_id = t.id);
COMMIT;
@@ -0,0 +1,248 @@
package me.iiv.agent.entity.m1;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* <p>
* 订单(m1 商城库)
* </p>
*
* @author codegen
* @since 2026-09-09
*/
@Getter
@Setter
@ToString
@TableName("mall_goods_order")
public class MallGoodsOrder implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/** 销售id */
private Long createId;
/** 创建时间 */
private Date createTime;
/** 修改id */
private Long modifyId;
/** 修改时间 */
private Date modifyTime;
/** 数据状态(0:启用,99:停用) */
private Integer softDelete;
/** 代理ID */
private Long agentId;
/** 物业ID */
private Long propertyId;
/** 站点ID */
private Long districtId;
/** 客户ID */
private Long userId;
/** 订单编号 */
private String orderNo;
/** 订单类型(0:代理;1:物业) */
private Integer orderType;
/** 结账类型(0:预付;1:月结) */
private Integer accountsType;
/** 支付方式 */
private Integer paymentType;
/** 收货地址 */
private Long addressId;
/** 订单状态(0:待付款;1:待出库;2:待发货;3:已发货;4:已签收;5:已收货;6:已取消;7:已删除) */
private Integer orderState;
/** 支付金额 */
private BigDecimal payMoney;
/** 已支付金额 */
private BigDecimal paidMoney;
/** 未支付金额 */
private BigDecimal unPayMoney;
/** 回款状态(0:待付款;1:已付款;2:付款中) */
private Integer payState;
/** 付款付款确认状态(0:未确认;1:已确认;2:延迟确认) */
private Integer orderPayState;
/** 退款状态(0:待退款;1:退款成功;2:退款失败;99:未知) */
private Integer rePayState;
/** 发票状态(0:未开票;1:已开票;2:部分开票) */
private Integer orderInvoiceState;
/** 订单有效时长(小时) */
private Integer effectiveTime;
/** 退款原因 */
private String reMemo;
/** 邮费 */
private BigDecimal postageMoney;
/** 订单备注 */
private String memo;
/** 应支付总金额 */
private BigDecimal totalMoney;
/** 商品金额 */
private BigDecimal goodsMoney;
/** 支付编号 */
private String payNo;
/** 下单时间 */
private Date orderTime;
/** 实际回款时间 */
private Date payTime;
/** 收货时间 */
private Date receiptTime;
/** 签收时间 */
private Date signTime;
/** 商品类型(0:非月结商品;1:月结商品) */
private Integer goodsType;
/** 账单编号(月结商品有) */
private String billNo;
/** 付款识别号(线下付款) */
private String payIdentifier;
/** 如果自行配送则有值 */
private String mobile;
/** 特许状态(0:未特许;1:已特许) */
private Integer franchiseState;
/** 优惠金额 */
private BigDecimal discountMoney;
/** 活动名称 */
private String activityName;
/** 活动编号 */
private String activityNo;
/** 促销编号 */
private String schemeNo;
/** 出库状态(0:未出库;1:已出库;2:部分出库) */
private Integer warehouseState;
/** 预订单编号 */
private String beforehandOrderNo;
/** 锁定状态(0:未锁定;1:已锁定) */
private Integer lockState;
/** 预订单下单人 */
private String placingPeople;
/** 预订单下单人 */
private Long placingPeopleId;
/** 代理 */
private String agentName;
/** 物业 */
private String propertyName;
/** 站点 */
private String districtName;
/** 下单人 */
private String userName;
/** 销售名称 */
private String createName;
/** 原始应支付金额 */
private BigDecimal originalPayMoney;
/** 逾期标识0:否;1:是 */
private Integer overdueFlag;
/** 逾期天数 */
private Integer overdueDays;
/** 回款周期(天) */
private Integer paymentCollection;
/** 最晚应回款时间 */
private Date lastPayTime;
/** 出库日期 */
private Date deliveryDate;
/** 金蝶系统内码 */
private Long jindieInternalId;
/** 金蝶系统采购订单内码 */
private Long jindiePurchaseInternalId;
/** 销售公司 */
private Long saleCompanyId;
/** 销售公司 */
private String saleCompany;
/** 发货公司 */
private Long deliveryCompanyId;
/** 发货公司 */
private String deliveryCompany;
/** 已开票金额 */
private BigDecimal invoiceMoney;
/** 未开票金额 */
private BigDecimal unInvoiceMoney;
/** 金蝶销售服务订单内码 */
private Long jindieServiceOrderInternalId;
/** 服务订单状态(0:未处理;1:已处理;2:无需处理) */
private Integer serviceOrderState;
/** 微店订单标识(0:否;1:是) */
private Integer weiDianOrderFlag;
/** 微店订单编号 */
private String weiDianOrderNo;
/** 装机单标识(0:否;1:是) */
private Integer installOrderFlag;
/** 营销状态(0:未营销;1:部分营销;2:已营销) */
private Integer marketingState;
}
@@ -0,0 +1,118 @@
package me.iiv.agent.entity.m1;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* <p>
* 订单商品详情(m1 商城库)
* </p>
*
* @author codegen
* @since 2026-09-09
*/
@Getter
@Setter
@ToString
@TableName("mall_order_details")
public class MallOrderDetails implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/** 创建id */
private Long createId;
/** 创建时间 */
private Date createTime;
/** 修改id */
private Long modifyId;
/** 修改时间 */
private Date modifyTime;
/** 数据状态(0:启用,99:停用) */
private Integer softDelete;
/** spu编号 */
private String spuNo;
/** sku编号 */
private String skuNo;
/** 购买数量 */
private Integer count;
/** 单价 */
private BigDecimal price;
/** 折扣 */
private Double discount;
/** 订单编号 */
private String orderNo;
/** 商品类型(0:非月结商品;1:月结商品) */
private Integer goodsType;
/** sku标签 */
private String skuLable;
/** 商品描述 */
private String skuDesc;
/** 封面路径 */
private String picUrl;
/** spu名称 */
private String spuName;
/** 物料类型(0:设备;1:配件;2:耗材);历史遗留驼峰列名 */
@TableField("spuType")
private Integer spuType;
/** 出库状态(0:未出库;1:已出库) */
private Integer warehouseState;
/** 出库单ID */
private Long warehouseOutId;
/** 原始单价 */
private BigDecimal originalPrice;
/** 原始购买数量 */
private Integer originalCount;
/** 收款状态(0:未收款;1:全部收款;2:部分收款) */
private Integer payState;
/** 发票状态(0:未开票;1:全部开票;2:部分开票) */
private Integer orderInvoiceState;
/** 应支付金额 */
private BigDecimal payMoney;
/** 应开票金额 */
private BigDecimal invoiceMoney;
/** 未支付金额 */
private BigDecimal notPayMoney;
/** 未开票金额 */
private BigDecimal notInvoiceMoney;
/** 营销状态(0:未营销;1:营销中;2:已营销) */
private Integer marketingState;
}
@@ -0,0 +1,93 @@
package me.iiv.agent.entity.m1;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 订单物流(m1 商城库)
* </p>
*
* @author codegen
* @since 2026-09-09
*/
@Getter
@Setter
@ToString
@TableName("mall_order_logistics")
public class MallOrderLogistics implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/** 订单发货人 */
private Long createId;
/** 创建时间 */
private Date createTime;
/** 修改id */
private Long modifyId;
/** 发货时间 */
private Date modifyTime;
/** 数据状态(0:启用,99:停用) */
private Integer softDelete;
/** 发货方式(0:快递公司;1:自行派送) */
private Integer sendMode;
/** 订单编号 */
private String orderNo;
/** 物流公司id */
private Long logisticsId;
/** 物流公司编号 */
private String logisticsNo;
/** 物流公司名称 */
private String logisticsName;
/** 快递单号 */
private String logisticsOrderNo;
/** 自己生成的编号 */
private String logisticsSelfNo;
/** 派送员姓名 */
private String deliverName;
/** 派送手机号 */
private String deliverNumber;
/** 配送员id */
private Long userId;
/** 物流当前状态(0:已出单;1:已出库;2:已发货;3:已揽件;4:运输中;5:待取件;6:派送中;7:已签收) */
private Integer state;
/** 收货方公司名称 */
private String receivingGoodsName;
/** 收货姓名 */
private String reveiveName;
/** 收货手机号 */
private String reveiveNumber;
/** 详细地址 */
private String address;
private Long agentId;
}
@@ -0,0 +1,62 @@
package me.iiv.agent.entity.m1;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 订单物流轨迹(m1 商城库;表名为老系统遗留拼写 mall_order_logistics_detai)。
* 注意:logistics_order_no 字段在老系统写入时混用,实际存的是订单号。
* </p>
*
* @author codegen
* @since 2026-09-09
*/
@Getter
@Setter
@ToString
@TableName("mall_order_logistics_detai")
public class MallOrderLogisticsDetai implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/** 创建id */
private Long createId;
/** 创建时间 */
private Date createTime;
/** 修改id */
private Long modifyId;
/** 修改时间 */
private Date modifyTime;
/** 数据状态(0:启用,99:停用) */
private Integer softDelete;
/** 快递单号(老系统写入时混用,实际多为订单号) */
private String logisticsOrderNo;
/** 物流详情 */
private String memo;
/** 物流状态(0:已出单;1:已出库;2:已发货;3:已揽件;4:运输中;5:待取件;6:派送中;7:已签收;13:上传凭证) */
private Integer state;
/** 订单号 */
private String orderNo;
/** 图片列表 */
private String picList;
}
@@ -0,0 +1,94 @@
package me.iiv.agent.entity.m1;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 收货地址(m1 商城库)
* </p>
*
* @author codegen
* @since 2026-09-09
*/
@Getter
@Setter
@ToString
@TableName("mall_shipping_address")
public class MallShippingAddress implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/** 创建id */
private Long createId;
/** 创建时间 */
private Date createTime;
/** 修改id */
private Long modifyId;
/** 修改时间 */
private Date modifyTime;
/** 数据状态(0:启用,99:停用) */
private Integer softDelete;
/** 收货姓名 */
private String reveiveName;
/** 收货手机号 */
private String reveiveNumber;
/** 客户id */
private Long userId;
/** 下单人 */
private String userName;
/** 所属站点 */
private Long districtId;
/** 省 */
private String province;
/** 市 */
private String city;
/** 县 */
private String countyseat;
/** 详细地址 */
private String address;
/** 备注信息 */
private String memo;
/** 状态 */
private Integer status;
/** 代理ID */
private Long agentId;
/** 默认标识0:否;1:是 */
private Integer defaultFlag;
/** 使用客户类型(0:内部人员;1:渠道商;2:直营客户) */
private Integer useCustomerType;
/** 使用客户ID */
private Long useCustomerId;
/** 使用客户名称 */
private String useCustomerName;
}
@@ -0,0 +1,16 @@
package me.iiv.agent.mapper.m1;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m1.MallGoodsOrder;
/**
* <p>
* MallGoodsOrder Mapper 接口
* </p>
*
* @author codegen
* @since 2026-09-09
*/
public interface MallGoodsOrderMapper extends BaseMapper<MallGoodsOrder> {
}
@@ -0,0 +1,16 @@
package me.iiv.agent.mapper.m1;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m1.MallOrderDetails;
/**
* <p>
* MallOrderDetails Mapper 接口
* </p>
*
* @author codegen
* @since 2026-09-09
*/
public interface MallOrderDetailsMapper extends BaseMapper<MallOrderDetails> {
}
@@ -0,0 +1,16 @@
package me.iiv.agent.mapper.m1;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m1.MallOrderLogisticsDetai;
/**
* <p>
* MallOrderLogisticsDetai Mapper 接口
* </p>
*
* @author codegen
* @since 2026-09-09
*/
public interface MallOrderLogisticsDetaiMapper extends BaseMapper<MallOrderLogisticsDetai> {
}
@@ -0,0 +1,16 @@
package me.iiv.agent.mapper.m1;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m1.MallOrderLogistics;
/**
* <p>
* MallOrderLogistics Mapper 接口
* </p>
*
* @author codegen
* @since 2026-09-09
*/
public interface MallOrderLogisticsMapper extends BaseMapper<MallOrderLogistics> {
}
@@ -0,0 +1,16 @@
package me.iiv.agent.mapper.m1;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import me.iiv.agent.entity.m1.MallShippingAddress;
/**
* <p>
* MallShippingAddress Mapper 接口
* </p>
*
* @author codegen
* @since 2026-09-09
*/
public interface MallShippingAddressMapper extends BaseMapper<MallShippingAddress> {
}
@@ -0,0 +1,426 @@
package me.iiv.agent.mcp.tool;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import me.iiv.agent.entity.m0.Agent;
import me.iiv.agent.entity.m0.Properyt;
import me.iiv.agent.entity.m0.User;
import me.iiv.agent.entity.m1.MallGoodsOrder;
import me.iiv.agent.entity.m1.MallOrderDetails;
import me.iiv.agent.entity.m1.MallOrderLogistics;
import me.iiv.agent.entity.m1.MallOrderLogisticsDetai;
import me.iiv.agent.entity.m1.MallShippingAddress;
import me.iiv.agent.mapper.m0.AgentMapper;
import me.iiv.agent.mapper.m0.ProperytMapper;
import me.iiv.agent.mapper.m1.MallGoodsOrderMapper;
import me.iiv.agent.mapper.m1.MallOrderDetailsMapper;
import me.iiv.agent.mapper.m1.MallOrderLogisticsDetaiMapper;
import me.iiv.agent.mapper.m1.MallOrderLogisticsMapper;
import me.iiv.agent.mapper.m1.MallShippingAddressMapper;
import me.iiv.agent.mcp.ToolObject;
import me.iiv.agent.mcp.XqTool;
import me.iiv.agent.permission.PermissionService;
import me.iiv.agent.security.AuthContextHolder;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* 商城订单域工具(m1 库 xiaoqu_mall_d):仿老系统 MallGoodsOrderController。
* 三个工具共用 PermissionService.applyOrderRowFilter(移植 WrapperUtils.getSaleCompany 语义)。
*/
@Component
public class OrderTools implements ToolObject {
private static final int DEFAULT_ROWS = 20;
private static final int MAX_ROWS = 100;
/** 商城业务时区固定 Asia/Shanghai,日期解析与时间格式化统一走该时区。 */
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
private static final Pattern DATE_PATTERN = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$");
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ISO_LOCAL_DATE.withResolverStyle(ResolverStyle.STRICT);
private static final DateTimeFormatter TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(BUSINESS_ZONE);
final MallGoodsOrderMapper orderMapper;
final MallOrderDetailsMapper orderDetailsMapper;
final MallShippingAddressMapper shippingAddressMapper;
final MallOrderLogisticsMapper logisticsMapper;
final MallOrderLogisticsDetaiMapper logisticsDetaiMapper;
final AgentMapper agentMapper;
final ProperytMapper properytMapper;
final PermissionService permissionService;
public OrderTools(MallGoodsOrderMapper orderMapper, MallOrderDetailsMapper orderDetailsMapper,
MallShippingAddressMapper shippingAddressMapper,
MallOrderLogisticsMapper logisticsMapper,
MallOrderLogisticsDetaiMapper logisticsDetaiMapper,
AgentMapper agentMapper, ProperytMapper properytMapper,
PermissionService permissionService) {
this.orderMapper = orderMapper;
this.orderDetailsMapper = orderDetailsMapper;
this.shippingAddressMapper = shippingAddressMapper;
this.logisticsMapper = logisticsMapper;
this.logisticsDetaiMapper = logisticsDetaiMapper;
this.agentMapper = agentMapper;
this.properytMapper = properytMapper;
this.permissionService = permissionService;
}
/**
* 订单列表,对应老接口 POST /mallGoodsOrder/listByPage。
* customerName 照搬老接口:orderType=0 取代理金蝶名称,否则取物业金蝶名称(批量富化,无 N+1)。
*/
@XqTool(category = "customer", kind = "read")
@Tool(name = "mall.order.list.query", description = "查询商城订单列表(分页),数据来源 m1 商城库 mall_goods_order 表。"
+ "行级权限:代理商身份(权限组 isagent=1)只能看自己+直属下级+授权代管代理的订单(有代管物业授权时可看对应物业订单);"
+ "销售部门非负责人只能看自己创建及代管代理名下物业的订单;总部身份(isagent=0)可看全部。"
+ "orderType 含义:0=代理 1=物业;orderState 含义:0=待付款 1=待出库 2=待发货 3=已发货 4=已签收 5=已收货 6=已取消 7=已删除;"
+ "payState 含义:0=待付款 1=已付款 2=付款中;warehouseState 含义:0=未出库 1=已出库 2=部分出库。"
+ "分页:page 从 1 开始,rows 默认 20、上限 100,超出自动截断为 100 并在返回的 notice 字段提示。"
+ "返回 total(符合条件的总数)、records(本页记录,按 id 倒序)。")
public Map<String, Object> mallOrderListQuery(
@ToolParam(required = false, description = "订单编号,模糊匹配") String orderNo,
@ToolParam(required = false, description = "代理商 id,精确匹配") Long agentId,
@ToolParam(required = false, description = "物业 id,精确匹配") Long propertyId,
@ToolParam(required = false, description = "订单类型:0=代理 1=物业") Integer orderType,
@ToolParam(required = false, description = "订单状态:0=待付款 1=待出库 2=待发货 3=已发货 4=已签收 5=已收货 6=已取消 7=已删除") Integer orderState,
@ToolParam(required = false, description = "回款状态:0=待付款 1=已付款 2=付款中") Integer payState,
@ToolParam(required = false, description = "下单时间起,格式 yyyy-MM-dd") String startOrderTime,
@ToolParam(required = false, description = "下单时间止,格式 yyyy-MM-dd(含当天)") String endOrderTime,
@ToolParam(required = false, description = "页码,从 1 开始,默认 1") Integer page,
@ToolParam(required = false, description = "每页条数,默认 20,上限 100(超出截断为 100)") Integer rows) {
Date startTime = parseDate(startOrderTime, "startOrderTime");
Date endTimeExclusive = parseNextDayStart(endOrderTime, "endOrderTime");
User user = AuthContextHolder.get().getUser();
int pageNo = page == null || page < 1 ? 1 : page;
boolean truncated = rows != null && rows > MAX_ROWS;
int pageSize = rows == null || rows < 1 ? DEFAULT_ROWS : Math.min(rows, MAX_ROWS);
LambdaQueryWrapper<MallGoodsOrder> wrapper = new LambdaQueryWrapper<MallGoodsOrder>()
.eq(MallGoodsOrder::getSoftDelete, 0);
if (StringUtils.hasText(orderNo)) {
wrapper.like(MallGoodsOrder::getOrderNo, orderNo);
}
if (agentId != null) {
wrapper.eq(MallGoodsOrder::getAgentId, agentId);
}
if (propertyId != null) {
wrapper.eq(MallGoodsOrder::getPropertyId, propertyId);
}
if (orderType != null) {
wrapper.eq(MallGoodsOrder::getOrderType, orderType);
}
if (orderState != null) {
wrapper.eq(MallGoodsOrder::getOrderState, orderState);
}
if (payState != null) {
wrapper.eq(MallGoodsOrder::getPayState, payState);
}
if (startTime != null) {
wrapper.ge(MallGoodsOrder::getOrderTime, startTime);
}
if (endTimeExclusive != null) {
wrapper.lt(MallGoodsOrder::getOrderTime, endTimeExclusive);
}
permissionService.applyOrderRowFilter(wrapper, user);
Long total = orderMapper.selectCount(wrapper);
wrapper.select(MallGoodsOrder::getId, MallGoodsOrder::getOrderNo, MallGoodsOrder::getOrderTime,
MallGoodsOrder::getOrderType, MallGoodsOrder::getAgentId, MallGoodsOrder::getAgentName,
MallGoodsOrder::getPropertyId, MallGoodsOrder::getPropertyName,
MallGoodsOrder::getSaleCompany, MallGoodsOrder::getPayMoney, MallGoodsOrder::getPaidMoney,
MallGoodsOrder::getUnPayMoney, MallGoodsOrder::getOrderState, MallGoodsOrder::getPayState,
MallGoodsOrder::getWarehouseState)
.orderByDesc(MallGoodsOrder::getId)
.last("LIMIT " + (long) (pageNo - 1) * pageSize + "," + pageSize);
List<MallGoodsOrder> orders = orderMapper.selectList(wrapper);
Map<Long, String> agentJinideNames = agentJinideNames(orders);
Map<Long, String> propertyJinideNames = propertyJinideNames(orders);
List<Map<String, Object>> records = new ArrayList<>();
for (MallGoodsOrder order : orders) {
Map<String, Object> record = new LinkedHashMap<>();
record.put("id", order.getId());
record.put("orderNo", order.getOrderNo());
record.put("orderTime", order.getOrderTime() == null ? null : formatTime(order.getOrderTime()));
record.put("orderType", order.getOrderType());
record.put("agentName", order.getAgentName());
record.put("propertyName", order.getPropertyName());
record.put("saleCompany", order.getSaleCompany());
record.put("payMoney", order.getPayMoney());
record.put("paidMoney", order.getPaidMoney());
record.put("unPayMoney", order.getUnPayMoney());
record.put("orderState", order.getOrderState());
record.put("payState", order.getPayState());
record.put("warehouseState", order.getWarehouseState());
record.put("customerName", customerName(order, agentJinideNames, propertyJinideNames));
records.add(record);
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("total", total);
result.put("page", pageNo);
result.put("rows", pageSize);
if (truncated) {
result.put("notice", "rows 超过上限 100,已截断为 100");
}
result.put("records", records);
return result;
}
/**
* 订单详情,合并老系统三个接口:订单主表 + orderDetailListByPage 明细 + getAddress 收货地址。
* 先按 orderNo + 行权限查订单主表,查不到即 record=null + notice(不区分不存在与无权限)。
*/
@XqTool(category = "customer", kind = "read")
@Tool(name = "mall.order.detail.query", description = "按订单号查询商城订单详情(含商品明细和收货地址),数据来源 m1 商城库。"
+ "行级权限与 mall.order.list.query 一致;订单不存在或当前身份无权查看时 record 为 null 并在 notice 字段说明(两者不区分)。"
+ "返回 record(订单关键字段 + 收货人姓名/电话/省市区/详细地址)和 items(商品明细列表)。")
public Map<String, Object> mallOrderDetailQuery(
@ToolParam(required = true, description = "订单编号(orderNo)") String orderNo) {
if (!StringUtils.hasText(orderNo)) {
throw new IllegalArgumentException("orderNo 必填");
}
User user = AuthContextHolder.get().getUser();
MallGoodsOrder order = findVisibleOrder(orderNo, user);
Map<String, Object> result = new LinkedHashMap<>();
if (order == null) {
result.put("record", null);
result.put("items", List.of());
result.put("notice", "orderNo=" + orderNo + " 不存在或当前身份无权查看");
return result;
}
MallShippingAddress address = order.getAddressId() == null
? null : shippingAddressMapper.selectById(order.getAddressId());
List<MallOrderDetails> details = orderDetailsMapper.selectList(
new LambdaQueryWrapper<MallOrderDetails>()
.eq(MallOrderDetails::getSoftDelete, 0)
.eq(MallOrderDetails::getOrderNo, orderNo)
.orderByAsc(MallOrderDetails::getId));
Map<String, Object> record = new LinkedHashMap<>();
record.put("id", order.getId());
record.put("orderNo", order.getOrderNo());
record.put("orderTime", order.getOrderTime() == null ? null : formatTime(order.getOrderTime()));
record.put("orderType", order.getOrderType());
record.put("agentId", order.getAgentId());
record.put("agentName", order.getAgentName());
record.put("propertyId", order.getPropertyId());
record.put("propertyName", order.getPropertyName());
record.put("districtId", order.getDistrictId());
record.put("districtName", order.getDistrictName());
record.put("userName", order.getUserName());
record.put("accountsType", order.getAccountsType());
record.put("paymentType", order.getPaymentType());
record.put("goodsType", order.getGoodsType());
record.put("saleCompanyId", order.getSaleCompanyId());
record.put("saleCompany", order.getSaleCompany());
record.put("payMoney", order.getPayMoney());
record.put("paidMoney", order.getPaidMoney());
record.put("unPayMoney", order.getUnPayMoney());
record.put("totalMoney", order.getTotalMoney());
record.put("goodsMoney", order.getGoodsMoney());
record.put("postageMoney", order.getPostageMoney());
record.put("discountMoney", order.getDiscountMoney());
record.put("orderState", order.getOrderState());
record.put("payState", order.getPayState());
record.put("orderPayState", order.getOrderPayState());
record.put("warehouseState", order.getWarehouseState());
record.put("orderInvoiceState", order.getOrderInvoiceState());
record.put("payNo", order.getPayNo());
record.put("payTime", order.getPayTime() == null ? null : formatTime(order.getPayTime()));
record.put("receiptTime", order.getReceiptTime() == null ? null : formatTime(order.getReceiptTime()));
record.put("signTime", order.getSignTime() == null ? null : formatTime(order.getSignTime()));
record.put("memo", order.getMemo());
record.put("createTime", order.getCreateTime() == null ? null : formatTime(order.getCreateTime()));
record.put("reveiveName", address == null ? null : address.getReveiveName());
record.put("reveiveNumber", address == null ? null : address.getReveiveNumber());
record.put("province", address == null ? null : address.getProvince());
record.put("city", address == null ? null : address.getCity());
record.put("countyseat", address == null ? null : address.getCountyseat());
record.put("address", address == null ? null : address.getAddress());
result.put("record", record);
List<Map<String, Object>> items = new ArrayList<>();
for (MallOrderDetails detail : details) {
Map<String, Object> item = new LinkedHashMap<>();
item.put("spuNo", detail.getSpuNo());
item.put("spuName", detail.getSpuName());
item.put("skuNo", detail.getSkuNo());
item.put("count", detail.getCount());
item.put("price", detail.getPrice());
item.put("payMoney", detail.getPayMoney());
item.put("payState", detail.getPayState());
item.put("warehouseState", detail.getWarehouseState());
items.add(item);
}
result.put("items", items);
return result;
}
/**
* 订单物流。轨迹表 mall_order_logistics_detai 的 logistics_order_no 字段
* 老系统写入时塞的是订单号(见 MallGoodsOrderController.confirmReceipt/mallGoodOrderSubmitCertificate),
* 故按 orderNo 查询,照搬老系统语义。
*/
@XqTool(category = "customer", kind = "read")
@Tool(name = "mall.order.logistics.query", description = "按订单号查询商城订单物流(发货信息 + 物流轨迹),数据来源 m1 商城库。"
+ "行级权限与 mall.order.list.query 一致;订单不存在或当前身份无权查看时 logistics 为 null 并在 notice 字段说明(两者不区分)。"
+ "返回 logistics(快递公司/快递单号/发货方式/当前状态/派送员,无物流记录时为 null)和 tracks(轨迹列表,按时间正序)。"
+ "sendMode 含义:0=快递 1=自送;state 含义:0=已出单 1=已出库 2=已发货 3=已揽件 4=运输中 5=待取件 6=派送中 7=已签收。"
+ "注意:物流轨迹为系统内记录,非实时快递查询,可能不全(菜鸟定时同步已停用)。")
public Map<String, Object> mallOrderLogisticsQuery(
@ToolParam(required = true, description = "订单编号(orderNo)") String orderNo) {
if (!StringUtils.hasText(orderNo)) {
throw new IllegalArgumentException("orderNo 必填");
}
User user = AuthContextHolder.get().getUser();
MallGoodsOrder order = findVisibleOrder(orderNo, user);
Map<String, Object> result = new LinkedHashMap<>();
if (order == null) {
result.put("logistics", null);
result.put("tracks", List.of());
result.put("notice", "orderNo=" + orderNo + " 不存在或当前身份无权查看");
return result;
}
List<MallOrderLogistics> logisticsList = logisticsMapper.selectList(
new LambdaQueryWrapper<MallOrderLogistics>()
.eq(MallOrderLogistics::getSoftDelete, 0)
.eq(MallOrderLogistics::getOrderNo, orderNo)
.orderByDesc(MallOrderLogistics::getId)
.last("LIMIT 1"));
MallOrderLogistics logistics = logisticsList.isEmpty() ? null : logisticsList.get(0);
if (logistics == null) {
result.put("logistics", null);
} else {
Map<String, Object> logisticsMap = new LinkedHashMap<>();
logisticsMap.put("logisticsName", logistics.getLogisticsName());
logisticsMap.put("logisticsOrderNo", logistics.getLogisticsOrderNo());
logisticsMap.put("sendMode", logistics.getSendMode());
logisticsMap.put("state", logistics.getState());
logisticsMap.put("deliverName", logistics.getDeliverName());
logisticsMap.put("deliverNumber", logistics.getDeliverNumber());
logisticsMap.put("sendTime", logistics.getModifyTime() == null ? null : formatTime(logistics.getModifyTime()));
logisticsMap.put("receivingGoodsName", logistics.getReceivingGoodsName());
logisticsMap.put("reveiveName", logistics.getReveiveName());
logisticsMap.put("reveiveNumber", logistics.getReveiveNumber());
logisticsMap.put("address", logistics.getAddress());
result.put("logistics", logisticsMap);
}
List<MallOrderLogisticsDetai> detaiList = logisticsDetaiMapper.selectList(
new LambdaQueryWrapper<MallOrderLogisticsDetai>()
.eq(MallOrderLogisticsDetai::getSoftDelete, 0)
.eq(MallOrderLogisticsDetai::getLogisticsOrderNo, orderNo)
.orderByAsc(MallOrderLogisticsDetai::getCreateTime)
.orderByAsc(MallOrderLogisticsDetai::getId));
List<Map<String, Object>> tracks = new ArrayList<>();
for (MallOrderLogisticsDetai detai : detaiList) {
Map<String, Object> track = new LinkedHashMap<>();
track.put("memo", detai.getMemo());
track.put("state", detai.getState());
track.put("picList", detai.getPicList());
track.put("createTime", detai.getCreateTime() == null ? null : formatTime(detai.getCreateTime()));
tracks.add(track);
}
result.put("tracks", tracks);
return result;
}
/** 按 orderNo + 行权限查订单主表;查不到返回 null(不区分不存在与无权限)。 */
private MallGoodsOrder findVisibleOrder(String orderNo, User user) {
LambdaQueryWrapper<MallGoodsOrder> wrapper = new LambdaQueryWrapper<MallGoodsOrder>()
.eq(MallGoodsOrder::getSoftDelete, 0)
.eq(MallGoodsOrder::getOrderNo, orderNo);
permissionService.applyOrderRowFilter(wrapper, user);
List<MallGoodsOrder> orders = orderMapper.selectList(
wrapper.orderByDesc(MallGoodsOrder::getId).last("LIMIT 1"));
return orders.isEmpty() ? null : orders.get(0);
}
/** customerName 照搬老接口 listByPage:orderType=0 取代理金蝶名称,否则取物业金蝶名称。 */
private static String customerName(MallGoodsOrder order, Map<Long, String> agentJinideNames,
Map<Long, String> propertyJinideNames) {
if (order.getOrderType() != null && order.getOrderType() == 0) {
return order.getAgentId() == null ? "" : agentJinideNames.getOrDefault(order.getAgentId(), "");
}
return order.getPropertyId() == null ? "" : propertyJinideNames.getOrDefault(order.getPropertyId(), "");
}
private Map<Long, String> agentJinideNames(List<MallGoodsOrder> orders) {
List<Long> ids = orders.stream()
.filter(order -> order.getOrderType() != null && order.getOrderType() == 0)
.map(MallGoodsOrder::getAgentId)
.filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (ids.isEmpty()) {
return Map.of();
}
return agentMapper.selectBatchIds(ids).stream()
.collect(Collectors.toMap(Agent::getId,
agent -> agent.getJinideName() == null ? "" : agent.getJinideName(), (a, b) -> a));
}
private Map<Long, String> propertyJinideNames(List<MallGoodsOrder> orders) {
List<Long> ids = orders.stream()
.filter(order -> order.getOrderType() == null || order.getOrderType() != 0)
.map(MallGoodsOrder::getPropertyId)
.filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (ids.isEmpty()) {
return Map.of();
}
return properytMapper.selectBatchIds(ids).stream()
.collect(Collectors.toMap(Properyt::getId,
property -> property.getJinideName() == null ? "" : property.getJinideName(), (a, b) -> a));
}
/** 严格校验 yyyy-MM-dd:先正则挡掉尾部垃圾字符,再 STRICT 解析(拒绝 2026-02-30 之类)。 */
private static Date parseDate(String value, String name) {
LocalDate date = parseLocalDate(value, name);
return date == null ? null : Date.from(date.atStartOfDay(BUSINESS_ZONE).toInstant());
}
/** 结束边界取「< 次日零点」,避免 23:59:59 写法漏掉当天末秒内的记录。 */
private static Date parseNextDayStart(String value, String name) {
LocalDate date = parseLocalDate(value, name);
return date == null ? null : Date.from(date.plusDays(1).atStartOfDay(BUSINESS_ZONE).toInstant());
}
private static LocalDate parseLocalDate(String value, String name) {
if (!StringUtils.hasText(value)) {
return null;
}
String trimmed = value.trim();
if (!DATE_PATTERN.matcher(trimmed).matches()) {
throw new IllegalArgumentException(name + " 格式必须为 yyyy-MM-dd");
}
try {
return LocalDate.parse(trimmed, DATE_FORMATTER);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException(name + " 格式必须为 yyyy-MM-dd");
}
}
private static String formatTime(Date date) {
return TIME_FORMATTER.format(date.toInstant());
}
}
@@ -7,6 +7,7 @@ import me.iiv.agent.entity.m0.Authority;
import me.iiv.agent.entity.m0.DistrictDistrictStrategy;
import me.iiv.agent.entity.m0.Properyt;
import me.iiv.agent.entity.m0.User;
import me.iiv.agent.entity.m1.MallGoodsOrder;
import me.iiv.agent.entity.m2.WarehouseAgentAuth;
import me.iiv.agent.mapper.m0.AgentManageDistrictAuthMapper;
import me.iiv.agent.mapper.m0.AgentMapper;
@@ -245,6 +246,60 @@ public class PermissionService {
}
}
/**
* 商城订单(m1 mall_goods_order 表)行级过滤,移植老系统 WrapperUtils.getSaleCompany
* (xiaoqu-franchisee utils/WrapperUtils.java:541-616,propertyFlag=true, className="property_id"):
* <ol>
* <li>isagent!=1(总部):不加任何过滤;</li>
* <li>销售部门非负责人(saleFlag 分支,:573-595,互斥 return):
* property_id IN (create_id=自己 的物业 ∪ [代管授权非空时] agent_id IN (下级+代管,移除自己) 的物业),
* 集合为空时 property_id IN (0);</li>
* <li>其余 isagent=1:sale_company_id IN (自己+下级+代管),
* 若存在 agent_manage_district_auth 代管物业则 AND (sale_company_id IN ids OR property_id IN 代管物业ids)。</li>
* </ol>
*/
public void applyOrderRowFilter(LambdaQueryWrapper<MallGoodsOrder> wrapper, User user) {
if (!isAgentAuthority(user.getAuthorityId())) {
return;
}
if (user.getAgentId() == null) {
throw new IllegalStateException("代理商用户未关联代理商,拒绝访问业务数据");
}
List<Long> ids = new ArrayList<>(subordinateAndSelfIds(user));
List<WarehouseAgentAuth> agentlist = managedAuths(user.getId());
ids.addAll(agentlist.stream().map(WarehouseAgentAuth::getManagerAgentId).collect(Collectors.toList()));
if (isSalesNonLeader(user)) {
List<Long> propertyIds = properytMapper.selectList(new LambdaQueryWrapper<Properyt>()
.eq(Properyt::getSoftDelete, 0)
.eq(Properyt::getCreateId, user.getId())
.select(Properyt::getId))
.stream().map(Properyt::getId).collect(Collectors.toList());
if (!agentlist.isEmpty()) {
ids.remove(user.getAgentId());
if (!ids.isEmpty()) {
propertyIds.addAll(properytMapper.selectList(new LambdaQueryWrapper<Properyt>()
.eq(Properyt::getSoftDelete, 0)
.in(Properyt::getAgentId, ids)
.select(Properyt::getId))
.stream().map(Properyt::getId).collect(Collectors.toList()));
}
}
wrapper.in(MallGoodsOrder::getPropertyId,
propertyIds.isEmpty() ? List.of(0L) : propertyIds);
return;
}
List<Long> managedPropertyIds = managedPropertyIds(ids);
if (!managedPropertyIds.isEmpty()) {
wrapper.and(w -> w.in(MallGoodsOrder::getSaleCompanyId, ids)
.or().in(MallGoodsOrder::getPropertyId, managedPropertyIds));
} else {
wrapper.in(MallGoodsOrder::getSaleCompanyId, ids);
}
}
/**
* 站点行权限:1:1 对齐老接口 districtlistByPage 使用的
* getByIsAgent(..., propertyFlag=true, className="property_id")。
@@ -0,0 +1,312 @@
package me.iiv.agent.mcp.tool;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import me.iiv.agent.entity.m0.Properyt;
import me.iiv.agent.entity.m0.User;
import me.iiv.agent.entity.m1.MallGoodsOrder;
import me.iiv.agent.entity.m1.MallOrderDetails;
import me.iiv.agent.entity.m1.MallOrderLogistics;
import me.iiv.agent.entity.m1.MallOrderLogisticsDetai;
import me.iiv.agent.entity.m1.MallShippingAddress;
import me.iiv.agent.mapper.m0.AgentMapper;
import me.iiv.agent.mapper.m0.ProperytMapper;
import me.iiv.agent.mapper.m1.MallGoodsOrderMapper;
import me.iiv.agent.mapper.m1.MallOrderDetailsMapper;
import me.iiv.agent.mapper.m1.MallOrderLogisticsDetaiMapper;
import me.iiv.agent.mapper.m1.MallOrderLogisticsMapper;
import me.iiv.agent.mapper.m1.MallShippingAddressMapper;
import me.iiv.agent.permission.PermissionService;
import me.iiv.agent.security.AuthContext;
import me.iiv.agent.security.AuthContextHolder;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class OrderToolsTest {
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
private static final DateTimeFormatter TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(BUSINESS_ZONE);
@Mock
MallGoodsOrderMapper orderMapper;
@Mock
MallOrderDetailsMapper orderDetailsMapper;
@Mock
MallShippingAddressMapper shippingAddressMapper;
@Mock
MallOrderLogisticsMapper logisticsMapper;
@Mock
MallOrderLogisticsDetaiMapper logisticsDetaiMapper;
@Mock
AgentMapper agentMapper;
@Mock
ProperytMapper properytMapper;
@Mock
PermissionService permissionService;
OrderTools tools;
@BeforeEach
void setUp() {
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
TableInfoHelper.initTableInfo(assistant, MallGoodsOrder.class);
TableInfoHelper.initTableInfo(assistant, MallOrderDetails.class);
TableInfoHelper.initTableInfo(assistant, MallOrderLogistics.class);
TableInfoHelper.initTableInfo(assistant, MallOrderLogisticsDetai.class);
tools = new OrderTools(orderMapper, orderDetailsMapper, shippingAddressMapper,
logisticsMapper, logisticsDetaiMapper, agentMapper, properytMapper, permissionService);
User user = new User();
user.setId(7L);
AuthContextHolder.set(new AuthContext(null, user));
}
@AfterEach
void tearDown() {
AuthContextHolder.clear();
}
@Test
void listTruncatesRowsAndAppliesOrderStateFilter() {
MallGoodsOrder order = new MallGoodsOrder();
order.setId(1L);
order.setOrderNo("SO-001");
order.setOrderType(1);
order.setPropertyId(11L);
order.setOrderState(3);
order.setPayState(1);
order.setWarehouseState(0);
order.setPayMoney(new BigDecimal("99.50"));
when(orderMapper.selectCount(any())).thenReturn(1L);
when(orderMapper.selectList(any())).thenReturn(List.of(order));
Properyt property = new Properyt();
property.setId(11L);
property.setJinideName("测试物业金蝶");
when(properytMapper.selectBatchIds(any())).thenReturn(List.of(property));
Map<String, Object> result = tools.mallOrderListQuery(
null, null, null, null, 3, null, null, null, 0, 500);
assertEquals(1, result.get("page"));
assertEquals(100, result.get("rows"));
assertEquals("rows 超过上限 100,已截断为 100", result.get("notice"));
assertEquals(1L, result.get("total"));
@SuppressWarnings("unchecked")
Map<String, Object> record = ((List<Map<String, Object>>) result.get("records")).get(0);
assertEquals("SO-001", record.get("orderNo"));
assertEquals(3, record.get("orderState"));
assertEquals("测试物业金蝶", record.get("customerName"));
@SuppressWarnings("unchecked")
ArgumentCaptor<LambdaQueryWrapper<MallGoodsOrder>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(orderMapper).selectList(captor.capture());
String where = captor.getValue().getSqlSegment();
assertTrue(where.contains("soft_delete"));
assertTrue(where.contains("order_state"));
verify(permissionService).applyOrderRowFilter(any(), any());
verifyNoInteractions(agentMapper);
}
@Test
void detailReturnsNullRecordWithNoticeWhenNotVisible() {
when(orderMapper.selectList(any())).thenReturn(List.of());
Map<String, Object> result = tools.mallOrderDetailQuery("SO-404");
assertNull(result.get("record"));
assertEquals(List.of(), result.get("items"));
assertEquals("orderNo=SO-404 不存在或当前身份无权查看", result.get("notice"));
verify(permissionService).applyOrderRowFilter(any(), any());
verifyNoInteractions(orderDetailsMapper, shippingAddressMapper);
}
@Test
void detailMergesShippingAddressAndItems() {
MallGoodsOrder order = new MallGoodsOrder();
order.setId(1L);
order.setOrderNo("SO-100");
order.setOrderType(1);
order.setAddressId(5L);
order.setOrderTime(new Date(1700000000000L));
when(orderMapper.selectList(any())).thenReturn(List.of(order));
MallShippingAddress address = new MallShippingAddress();
address.setId(5L);
address.setReveiveName("张三");
address.setReveiveNumber("13800000000");
address.setProvince("广东省");
address.setCity("深圳市");
address.setCountyseat("南山区");
address.setAddress("科技园 1 栋");
when(shippingAddressMapper.selectById(5L)).thenReturn(address);
MallOrderDetails detail = new MallOrderDetails();
detail.setId(9L);
detail.setSpuNo("SPU-1");
detail.setSpuName("大卷纸");
detail.setSkuNo("SKU-1");
detail.setCount(2);
detail.setPrice(new BigDecimal("10.00"));
detail.setPayMoney(new BigDecimal("20.00"));
detail.setPayState(1);
detail.setWarehouseState(0);
when(orderDetailsMapper.selectList(any())).thenReturn(List.of(detail));
Map<String, Object> result = tools.mallOrderDetailQuery("SO-100");
@SuppressWarnings("unchecked")
Map<String, Object> record = (Map<String, Object>) result.get("record");
assertEquals("SO-100", record.get("orderNo"));
assertEquals("张三", record.get("reveiveName"));
assertEquals("13800000000", record.get("reveiveNumber"));
assertEquals("南山区", record.get("countyseat"));
assertEquals("科技园 1 栋", record.get("address"));
assertEquals(TIME_FORMATTER.format(new Date(1700000000000L).toInstant()),
record.get("orderTime"));
@SuppressWarnings("unchecked")
List<Map<String, Object>> items = (List<Map<String, Object>>) result.get("items");
assertEquals(1, items.size());
assertEquals("SPU-1", items.get(0).get("spuNo"));
assertEquals(2, items.get(0).get("count"));
verify(permissionService).applyOrderRowFilter(any(), any());
}
@Test
void logisticsReturnsNullLogisticsWhenNoRecord() {
MallGoodsOrder order = new MallGoodsOrder();
order.setId(1L);
order.setOrderNo("SO-200");
when(orderMapper.selectList(any())).thenReturn(List.of(order));
when(logisticsMapper.selectList(any())).thenReturn(List.of());
when(logisticsDetaiMapper.selectList(any())).thenReturn(List.of());
Map<String, Object> result = tools.mallOrderLogisticsQuery("SO-200");
assertNull(result.get("logistics"));
assertEquals(List.of(), result.get("tracks"));
verify(permissionService).applyOrderRowFilter(any(), any());
}
@Test
void logisticsTracksQueriedByOrderNoAsc() {
MallGoodsOrder order = new MallGoodsOrder();
order.setId(1L);
order.setOrderNo("SO-300");
when(orderMapper.selectList(any())).thenReturn(List.of(order));
MallOrderLogistics logistics = new MallOrderLogistics();
logistics.setId(3L);
logistics.setOrderNo("SO-300");
logistics.setLogisticsName("顺丰速运");
logistics.setLogisticsOrderNo("SF-123");
logistics.setSendMode(0);
logistics.setState(4);
logistics.setModifyTime(new Date(1700000100000L));
when(logisticsMapper.selectList(any())).thenReturn(List.of(logistics));
// 老系统语义:轨迹表 logistics_order_no 存的是订单号而非快递单号
MallOrderLogisticsDetai track = new MallOrderLogisticsDetai();
track.setId(8L);
track.setLogisticsOrderNo("SO-300");
track.setMemo("包裹已到达深圳转运中心");
track.setState(4);
track.setCreateTime(new Date(1700000200000L));
when(logisticsDetaiMapper.selectList(any())).thenReturn(List.of(track));
Map<String, Object> result = tools.mallOrderLogisticsQuery("SO-300");
@SuppressWarnings("unchecked")
Map<String, Object> logisticsMap = (Map<String, Object>) result.get("logistics");
assertEquals("顺丰速运", logisticsMap.get("logisticsName"));
assertEquals("SF-123", logisticsMap.get("logisticsOrderNo"));
assertEquals(4, logisticsMap.get("state"));
@SuppressWarnings("unchecked")
List<Map<String, Object>> tracks = (List<Map<String, Object>>) result.get("tracks");
assertEquals(1, tracks.size());
assertEquals("包裹已到达深圳转运中心", tracks.get(0).get("memo"));
assertEquals(TIME_FORMATTER.format(new Date(1700000200000L).toInstant()),
tracks.get(0).get("createTime"));
@SuppressWarnings("unchecked")
ArgumentCaptor<LambdaQueryWrapper<MallOrderLogisticsDetai>> captor =
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(logisticsDetaiMapper).selectList(captor.capture());
LambdaQueryWrapper<MallOrderLogisticsDetai> wrapper = captor.getValue();
assertTrue(wrapper.getSqlSegment().contains("logistics_order_no"));
String orderBy = wrapper.getExpression().getOrderBy().getSqlSegment();
assertTrue(orderBy.contains("create_time ASC"));
assertTrue(orderBy.contains("id ASC"));
verify(permissionService).applyOrderRowFilter(any(), any());
}
@Test
void listAppliesDateRangeInShanghaiZoneWithExclusiveEnd() {
when(orderMapper.selectCount(any())).thenReturn(0L);
when(orderMapper.selectList(any())).thenReturn(List.of());
Map<String, Object> result = tools.mallOrderListQuery(
null, null, null, null, null, null, "2026-09-01", "2026-09-09", null, null);
assertEquals(0L, result.get("total"));
@SuppressWarnings("unchecked")
ArgumentCaptor<LambdaQueryWrapper<MallGoodsOrder>> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(orderMapper).selectList(captor.capture());
LambdaQueryWrapper<MallGoodsOrder> wrapper = captor.getValue();
String where = wrapper.getSqlSegment();
assertTrue(where.contains("order_time >="));
assertTrue(where.contains("order_time <"));
Date expectedStart = Date.from(LocalDate.of(2026, 9, 1).atStartOfDay(BUSINESS_ZONE).toInstant());
// 结束边界为「< 次日零点」(Asia/Shanghai),而非 <= 当天 23:59:59
Date expectedEndExclusive = Date.from(LocalDate.of(2026, 9, 10).atStartOfDay(BUSINESS_ZONE).toInstant());
assertTrue(wrapper.getParamNameValuePairs().values().contains(expectedStart));
assertTrue(wrapper.getParamNameValuePairs().values().contains(expectedEndExclusive));
}
@Test
void listRejectsDateParamWithTrailingJunk() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () ->
tools.mallOrderListQuery(null, null, null, null, null, null, "2026-09-09junk", null, null, null));
assertEquals("startOrderTime 格式必须为 yyyy-MM-dd", ex.getMessage());
assertThrows(IllegalArgumentException.class, () ->
tools.mallOrderListQuery(null, null, null, null, null, null, null, "2026-9-9", null, null));
verifyNoInteractions(orderMapper);
}
@Test
void listRejectsNonexistentCalendarDate() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () ->
tools.mallOrderListQuery(null, null, null, null, null, null, null, "2026-02-30", null, null));
assertEquals("endOrderTime 格式必须为 yyyy-MM-dd", ex.getMessage());
verifyNoInteractions(orderMapper);
}
@Test
void detailAndLogisticsRequireOrderNo() {
assertThrows(IllegalArgumentException.class, () -> tools.mallOrderDetailQuery(null));
assertThrows(IllegalArgumentException.class, () -> tools.mallOrderDetailQuery(" "));
assertThrows(IllegalArgumentException.class, () -> tools.mallOrderLogisticsQuery(null));
assertThrows(IllegalArgumentException.class, () -> tools.mallOrderLogisticsQuery(" "));
verifyNoInteractions(orderMapper);
}
}
@@ -0,0 +1,197 @@
package me.iiv.agent.permission;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import me.iiv.agent.entity.m0.Agent;
import me.iiv.agent.entity.m0.AgentManageDistrictAuth;
import me.iiv.agent.entity.m0.Authority;
import me.iiv.agent.entity.m0.Properyt;
import me.iiv.agent.entity.m0.User;
import me.iiv.agent.entity.m1.MallGoodsOrder;
import me.iiv.agent.entity.m2.WarehouseAgentAuth;
import me.iiv.agent.mapper.m0.AgentManageDistrictAuthMapper;
import me.iiv.agent.mapper.m0.AgentMapper;
import me.iiv.agent.mapper.m0.AuthorityMapper;
import me.iiv.agent.mapper.m0.ProperytMapper;
import me.iiv.agent.mapper.m2.WarehouseAgentAuthMapper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.Collection;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
/** PermissionService.applyOrderRowFilter 直接单测:商城订单(mall_goods_order)行级权限各分支。 */
@ExtendWith(MockitoExtension.class)
class PermissionServiceOrderRowFilterTest {
@Mock
AuthorityMapper authorityMapper;
@Mock
AgentMapper agentMapper;
@Mock
WarehouseAgentAuthMapper warehouseAgentAuthMapper;
@Mock
AgentManageDistrictAuthMapper agentManageDistrictAuthMapper;
@Mock
ProperytMapper properytMapper;
@Mock
StringRedisTemplate redisTemplate;
@Mock
HashOperations<String, Object, Object> hashOperations;
PermissionService permissionService;
@BeforeEach
void setUp() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
MallGoodsOrder.class);
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""),
Properyt.class);
permissionService = new PermissionService(authorityMapper, agentMapper,
warehouseAgentAuthMapper, agentManageDistrictAuthMapper, properytMapper, redisTemplate);
}
@Test
void headquartersOrderFilterAddsNoBoundary() {
User user = user(46L, 1L, null);
when(authorityMapper.selectById(1L)).thenReturn(authority(1L, 0));
LambdaQueryWrapper<MallGoodsOrder> wrapper = new LambdaQueryWrapper<>();
permissionService.applyOrderRowFilter(wrapper, user);
assertTrue(wrapper.getSqlSegment().isBlank());
}
@Test
void agentOrderFilterUsesSelfSubordinateAndManagedSaleCompanies() {
User user = user(1000L, 108L, 1L);
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
Agent subordinate = new Agent();
subordinate.setId(2L);
when(agentMapper.selectList(any())).thenReturn(List.of(subordinate));
WarehouseAgentAuth managedAuth = new WarehouseAgentAuth();
managedAuth.setManagerAgentId(3L);
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of(managedAuth));
when(agentManageDistrictAuthMapper.selectList(any())).thenReturn(List.of());
LambdaQueryWrapper<MallGoodsOrder> wrapper = new LambdaQueryWrapper<>();
permissionService.applyOrderRowFilter(wrapper, user);
String sql = wrapper.getSqlSegment();
Collection<Object> values = wrapper.getParamNameValuePairs().values();
assertTrue(sql.contains("sale_company_id IN"));
assertFalse(sql.contains("property_id"));
assertTrue(values.contains(1L));
assertTrue(values.contains(2L));
assertTrue(values.contains(3L));
}
@Test
void agentOrderFilterAddsManagedPropertyOrBranch() {
User user = user(1000L, 108L, 1L);
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
when(agentMapper.selectList(any())).thenReturn(List.of());
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of());
AgentManageDistrictAuth districtAuth = new AgentManageDistrictAuth();
districtAuth.setPropertyId(44L);
when(agentManageDistrictAuthMapper.selectList(any())).thenReturn(List.of(districtAuth));
LambdaQueryWrapper<MallGoodsOrder> wrapper = new LambdaQueryWrapper<>();
permissionService.applyOrderRowFilter(wrapper, user);
String sql = wrapper.getSqlSegment();
Collection<Object> values = wrapper.getParamNameValuePairs().values();
assertTrue(sql.contains("sale_company_id IN"));
assertTrue(sql.contains("property_id IN"));
assertTrue(sql.contains("OR"));
assertTrue(values.contains(1L));
assertTrue(values.contains(44L));
}
@Test
void salesNonLeaderOrderFilterUsesOwnCreatedPropertyIds() {
User user = user(1000L, 108L, 1L);
user.setDeptId(5L);
user.setPostId(7L);
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
when(agentMapper.selectList(any())).thenReturn(List.of());
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of());
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(hashOperations.get("Dictionary", "authDeptId")).thenReturn(",5,");
when(hashOperations.get("Dictionary", "authDeptPostId")).thenReturn("8,");
Properyt property = new Properyt();
property.setId(55L);
when(properytMapper.selectList(any())).thenReturn(List.of(property));
LambdaQueryWrapper<MallGoodsOrder> wrapper = new LambdaQueryWrapper<>();
permissionService.applyOrderRowFilter(wrapper, user);
String sql = wrapper.getSqlSegment();
assertTrue(sql.contains("property_id IN"));
assertFalse(sql.contains("sale_company_id"));
assertTrue(wrapper.getParamNameValuePairs().values().contains(55L));
verifyNoInteractions(agentManageDistrictAuthMapper);
}
@Test
void salesNonLeaderWithoutVisiblePropertiesFiltersToZero() {
User user = user(1000L, 108L, 1L);
user.setDeptId(5L);
user.setPostId(7L);
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
when(agentMapper.selectList(any())).thenReturn(List.of());
when(warehouseAgentAuthMapper.selectList(any())).thenReturn(List.of());
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
when(hashOperations.get("Dictionary", "authDeptId")).thenReturn(",5,");
when(hashOperations.get("Dictionary", "authDeptPostId")).thenReturn("8,");
when(properytMapper.selectList(any())).thenReturn(List.of());
LambdaQueryWrapper<MallGoodsOrder> wrapper = new LambdaQueryWrapper<>();
permissionService.applyOrderRowFilter(wrapper, user);
String sql = wrapper.getSqlSegment();
assertTrue(sql.contains("property_id IN"));
assertFalse(sql.contains("sale_company_id"));
assertTrue(wrapper.getParamNameValuePairs().values().contains(0L));
verifyNoInteractions(agentManageDistrictAuthMapper);
}
@Test
void agentUserWithoutAgentIdIsDeniedForOrderFilter() {
User user = user(1000L, 108L, null);
when(authorityMapper.selectById(108L)).thenReturn(authority(108L, 1));
assertThrows(IllegalStateException.class,
() -> permissionService.applyOrderRowFilter(new LambdaQueryWrapper<>(), user));
}
private static Authority authority(Long id, int isAgent) {
Authority authority = new Authority();
authority.setId(id);
authority.setIsagent(isAgent);
authority.setSoftDelete(0);
return authority;
}
private static User user(Long id, Long authorityId, Long agentId) {
User user = new User();
user.setId(id);
user.setAuthorityId(authorityId);
user.setAgentId(agentId);
return user;
}
}