From 3d6d70604d85dd531b598fcfcd729764ed498871 Mon Sep 17 00:00:00 2001 From: xiaoqu Date: Wed, 9 Sep 2026 18:06:59 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(mcp):=20=E6=96=B0=E5=A2=9E=E5=95=86?= =?UTF-8?q?=E5=9F=8E=E8=AE=A2=E5=8D=95=E6=9F=A5=E8=AF=A2=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=20mall.order.list/detail/logistics.query=20(REQ-20260909-0004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OrderTools: 订单列表(9个过滤字段+分页)、订单详情(订单+明细+收货地址合并)、订单物流(主表+轨迹) - 新增 m1 商城库 5 个 Entity/Mapper(首次启用 m1 mapper 包) - PermissionService.applyOrderRowFilter: 移植老接口 getSaleCompany 行级权限语义 - OrderToolsTest 6 用例;seed-dev.sql 绑定 operation scope;文档第 7~9 节 --- .../doc/agent-mcp-tools-reference.md | 82 ++++ xiaoqu-agent-service/sql/seed-dev.sql | 3 +- .../iiv/agent/entity/m1/MallGoodsOrder.java | 248 +++++++++++ .../iiv/agent/entity/m1/MallOrderDetails.java | 118 +++++ .../agent/entity/m1/MallOrderLogistics.java | 93 ++++ .../entity/m1/MallOrderLogisticsDetai.java | 62 +++ .../agent/entity/m1/MallShippingAddress.java | 94 ++++ .../agent/mapper/m1/MallGoodsOrderMapper.java | 16 + .../mapper/m1/MallOrderDetailsMapper.java | 16 + .../m1/MallOrderLogisticsDetaiMapper.java | 16 + .../mapper/m1/MallOrderLogisticsMapper.java | 16 + .../mapper/m1/MallShippingAddressMapper.java | 16 + .../me/iiv/agent/mcp/tool/OrderTools.java | 405 ++++++++++++++++++ .../agent/permission/PermissionService.java | 55 +++ .../me/iiv/agent/mcp/tool/OrderToolsTest.java | 265 ++++++++++++ 15 files changed, 1504 insertions(+), 1 deletion(-) create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallGoodsOrder.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderDetails.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderLogistics.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderLogisticsDetai.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallShippingAddress.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallGoodsOrderMapper.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderDetailsMapper.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderLogisticsDetaiMapper.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderLogisticsMapper.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallShippingAddressMapper.java create mode 100644 xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/OrderTools.java create mode 100644 xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/OrderToolsTest.java diff --git a/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md b/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md index 6f165a4..d643a6f 100644 --- a/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md +++ b/xiaoqu-agent-service/doc/agent-mcp-tools-reference.md @@ -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 diff --git a/xiaoqu-agent-service/sql/seed-dev.sql b/xiaoqu-agent-service/sql/seed-dev.sql index 9a6bb82..a1aa9b1 100644 --- a/xiaoqu-agent-service/sql/seed-dev.sql +++ b/xiaoqu-agent-service/sql/seed-dev.sql @@ -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; diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallGoodsOrder.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallGoodsOrder.java new file mode 100644 index 0000000..aad9b2c --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallGoodsOrder.java @@ -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; + +/** + *

+ * 订单(m1 商城库) + *

+ * + * @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; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderDetails.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderDetails.java new file mode 100644 index 0000000..457536a --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderDetails.java @@ -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; + +/** + *

+ * 订单商品详情(m1 商城库) + *

+ * + * @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; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderLogistics.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderLogistics.java new file mode 100644 index 0000000..ef4584c --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderLogistics.java @@ -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; + +/** + *

+ * 订单物流(m1 商城库) + *

+ * + * @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; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderLogisticsDetai.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderLogisticsDetai.java new file mode 100644 index 0000000..41f5e5e --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallOrderLogisticsDetai.java @@ -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; + +/** + *

+ * 订单物流轨迹(m1 商城库;表名为老系统遗留拼写 mall_order_logistics_detai)。 + * 注意:logistics_order_no 字段在老系统写入时混用,实际存的是订单号。 + *

+ * + * @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; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallShippingAddress.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallShippingAddress.java new file mode 100644 index 0000000..84379aa --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/entity/m1/MallShippingAddress.java @@ -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; + +/** + *

+ * 收货地址(m1 商城库) + *

+ * + * @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; +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallGoodsOrderMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallGoodsOrderMapper.java new file mode 100644 index 0000000..84699eb --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallGoodsOrderMapper.java @@ -0,0 +1,16 @@ +package me.iiv.agent.mapper.m1; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m1.MallGoodsOrder; + +/** + *

+ * MallGoodsOrder Mapper 接口 + *

+ * + * @author codegen + * @since 2026-09-09 + */ +public interface MallGoodsOrderMapper extends BaseMapper { + +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderDetailsMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderDetailsMapper.java new file mode 100644 index 0000000..3828aa4 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderDetailsMapper.java @@ -0,0 +1,16 @@ +package me.iiv.agent.mapper.m1; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m1.MallOrderDetails; + +/** + *

+ * MallOrderDetails Mapper 接口 + *

+ * + * @author codegen + * @since 2026-09-09 + */ +public interface MallOrderDetailsMapper extends BaseMapper { + +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderLogisticsDetaiMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderLogisticsDetaiMapper.java new file mode 100644 index 0000000..4c59dba --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderLogisticsDetaiMapper.java @@ -0,0 +1,16 @@ +package me.iiv.agent.mapper.m1; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m1.MallOrderLogisticsDetai; + +/** + *

+ * MallOrderLogisticsDetai Mapper 接口 + *

+ * + * @author codegen + * @since 2026-09-09 + */ +public interface MallOrderLogisticsDetaiMapper extends BaseMapper { + +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderLogisticsMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderLogisticsMapper.java new file mode 100644 index 0000000..97c6e15 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallOrderLogisticsMapper.java @@ -0,0 +1,16 @@ +package me.iiv.agent.mapper.m1; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m1.MallOrderLogistics; + +/** + *

+ * MallOrderLogistics Mapper 接口 + *

+ * + * @author codegen + * @since 2026-09-09 + */ +public interface MallOrderLogisticsMapper extends BaseMapper { + +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallShippingAddressMapper.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallShippingAddressMapper.java new file mode 100644 index 0000000..056476b --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mapper/m1/MallShippingAddressMapper.java @@ -0,0 +1,16 @@ +package me.iiv.agent.mapper.m1; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import me.iiv.agent.entity.m1.MallShippingAddress; + +/** + *

+ * MallShippingAddress Mapper 接口 + *

+ * + * @author codegen + * @since 2026-09-09 + */ +public interface MallShippingAddressMapper extends BaseMapper { + +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/OrderTools.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/OrderTools.java new file mode 100644 index 0000000..d9647f0 --- /dev/null +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/OrderTools.java @@ -0,0 +1,405 @@ +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.text.ParseException; +import java.text.SimpleDateFormat; +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.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; + private static final String DATE_FORMAT = "yyyy-MM-dd"; + + 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 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 endTime = parseDate(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 wrapper = new LambdaQueryWrapper() + .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 (endTime != null) { + wrapper.le(MallGoodsOrder::getOrderTime, endOfDay(endTime)); + } + 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 orders = orderMapper.selectList(wrapper); + + Map agentJinideNames = agentJinideNames(orders); + Map propertyJinideNames = propertyJinideNames(orders); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + List> records = new ArrayList<>(); + for (MallGoodsOrder order : orders) { + Map record = new LinkedHashMap<>(); + record.put("id", order.getId()); + record.put("orderNo", order.getOrderNo()); + record.put("orderTime", order.getOrderTime() == null ? null : sdf.format(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 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 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 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 details = orderDetailsMapper.selectList( + new LambdaQueryWrapper() + .eq(MallOrderDetails::getSoftDelete, 0) + .eq(MallOrderDetails::getOrderNo, orderNo) + .orderByAsc(MallOrderDetails::getId)); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Map record = new LinkedHashMap<>(); + record.put("id", order.getId()); + record.put("orderNo", order.getOrderNo()); + record.put("orderTime", order.getOrderTime() == null ? null : sdf.format(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 : sdf.format(order.getPayTime())); + record.put("receiptTime", order.getReceiptTime() == null ? null : sdf.format(order.getReceiptTime())); + record.put("signTime", order.getSignTime() == null ? null : sdf.format(order.getSignTime())); + record.put("memo", order.getMemo()); + record.put("createTime", order.getCreateTime() == null ? null : sdf.format(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> items = new ArrayList<>(); + for (MallOrderDetails detail : details) { + Map 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 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 result = new LinkedHashMap<>(); + if (order == null) { + result.put("logistics", null); + result.put("tracks", List.of()); + result.put("notice", "orderNo=" + orderNo + " 不存在或当前身份无权查看"); + return result; + } + + List logisticsList = logisticsMapper.selectList( + new LambdaQueryWrapper() + .eq(MallOrderLogistics::getSoftDelete, 0) + .eq(MallOrderLogistics::getOrderNo, orderNo) + .orderByDesc(MallOrderLogistics::getId) + .last("LIMIT 1")); + MallOrderLogistics logistics = logisticsList.isEmpty() ? null : logisticsList.get(0); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + if (logistics == null) { + result.put("logistics", null); + } else { + Map 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 : sdf.format(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 detaiList = logisticsDetaiMapper.selectList( + new LambdaQueryWrapper() + .eq(MallOrderLogisticsDetai::getSoftDelete, 0) + .eq(MallOrderLogisticsDetai::getLogisticsOrderNo, orderNo) + .orderByAsc(MallOrderLogisticsDetai::getCreateTime) + .orderByAsc(MallOrderLogisticsDetai::getId)); + List> tracks = new ArrayList<>(); + for (MallOrderLogisticsDetai detai : detaiList) { + Map 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 : sdf.format(detai.getCreateTime())); + tracks.add(track); + } + result.put("tracks", tracks); + return result; + } + + /** 按 orderNo + 行权限查订单主表;查不到返回 null(不区分不存在与无权限)。 */ + private MallGoodsOrder findVisibleOrder(String orderNo, User user) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(MallGoodsOrder::getSoftDelete, 0) + .eq(MallGoodsOrder::getOrderNo, orderNo); + permissionService.applyOrderRowFilter(wrapper, user); + List 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 agentJinideNames, + Map 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 agentJinideNames(List orders) { + List 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 propertyJinideNames(List orders) { + List 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)); + } + + private static Date parseDate(String value, String name) { + if (!StringUtils.hasText(value)) { + return null; + } + SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); + sdf.setLenient(false); + try { + return sdf.parse(value.trim()); + } catch (ParseException e) { + throw new IllegalArgumentException(name + " 格式必须为 yyyy-MM-dd"); + } + } + + private static Date endOfDay(Date date) { + return new Date(date.getTime() + 24L * 3600 * 1000 - 1000); + } +} diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java index f36e54d..fe503dd 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/permission/PermissionService.java @@ -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"): + *
    + *
  1. isagent!=1(总部):不加任何过滤;
  2. + *
  3. 销售部门非负责人(saleFlag 分支,:573-595,互斥 return): + * property_id IN (create_id=自己 的物业 ∪ [代管授权非空时] agent_id IN (下级+代管,移除自己) 的物业), + * 集合为空时 property_id IN (0);
  4. + *
  5. 其余 isagent=1:sale_company_id IN (自己+下级+代管), + * 若存在 agent_manage_district_auth 代管物业则 AND (sale_company_id IN ids OR property_id IN 代管物业ids)。
  6. + *
+ */ + public void applyOrderRowFilter(LambdaQueryWrapper wrapper, User user) { + if (!isAgentAuthority(user.getAuthorityId())) { + return; + } + if (user.getAgentId() == null) { + throw new IllegalStateException("代理商用户未关联代理商,拒绝访问业务数据"); + } + + List ids = new ArrayList<>(subordinateAndSelfIds(user)); + List agentlist = managedAuths(user.getId()); + ids.addAll(agentlist.stream().map(WarehouseAgentAuth::getManagerAgentId).collect(Collectors.toList())); + + if (isSalesNonLeader(user)) { + List propertyIds = properytMapper.selectList(new LambdaQueryWrapper() + .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() + .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 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")。 diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/OrderToolsTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/OrderToolsTest.java new file mode 100644 index 0000000..0118eb7 --- /dev/null +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/OrderToolsTest.java @@ -0,0 +1,265 @@ +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.text.SimpleDateFormat; +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 { + + @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 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 record = ((List>) result.get("records")).get(0); + assertEquals("SO-001", record.get("orderNo")); + assertEquals(3, record.get("orderState")); + assertEquals("测试物业金蝶", record.get("customerName")); + + @SuppressWarnings("unchecked") + ArgumentCaptor> 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 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 result = tools.mallOrderDetailQuery("SO-100"); + + @SuppressWarnings("unchecked") + Map record = (Map) 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(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(1700000000000L)), + record.get("orderTime")); + @SuppressWarnings("unchecked") + List> items = (List>) 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 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 result = tools.mallOrderLogisticsQuery("SO-300"); + + @SuppressWarnings("unchecked") + Map logisticsMap = (Map) result.get("logistics"); + assertEquals("顺丰速运", logisticsMap.get("logisticsName")); + assertEquals("SF-123", logisticsMap.get("logisticsOrderNo")); + assertEquals(4, logisticsMap.get("state")); + @SuppressWarnings("unchecked") + List> tracks = (List>) result.get("tracks"); + assertEquals(1, tracks.size()); + assertEquals("包裹已到达深圳转运中心", tracks.get(0).get("memo")); + assertEquals(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(1700000200000L)), + tracks.get(0).get("createTime")); + + @SuppressWarnings("unchecked") + ArgumentCaptor> captor = + ArgumentCaptor.forClass(LambdaQueryWrapper.class); + verify(logisticsDetaiMapper).selectList(captor.capture()); + LambdaQueryWrapper 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 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); + } +} From af22fd0e4521b6b5a64babb3e06217a54b73300d Mon Sep 17 00:00:00 2001 From: xiaoqu Date: Wed, 9 Sep 2026 18:57:09 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(mcp):=20=E4=BF=AE=E5=A4=8D=E5=95=86?= =?UTF-8?q?=E5=9F=8E=E8=AE=A2=E5=8D=95=E5=B7=A5=E5=85=B7=20CR=20=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E2=80=94=E2=80=94=E6=97=B6=E5=8C=BA=E5=9B=BA=E5=AE=9A?= =?UTF-8?q?/=E4=B8=A5=E6=A0=BC=E6=97=A5=E6=9C=9F=E6=A0=A1=E9=AA=8C/?= =?UTF-8?q?=E6=9D=83=E9=99=90=E5=8D=95=E6=B5=8B=20(REQ-20260909-0004)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OrderTools: startOrderTime/endOrderTime 解析与输出时间格式化统一固定 Asia/Shanghai; endOrderTime 边界改为 < 次日零点(LocalDate.atStartOfDay),不再用 23:59:59 写法 - 日期入参严格校验:正则 ^\d{4}-\d{2}-\d{2}$ 全串匹配 + ISO_LOCAL_DATE/STRICT 解析, 拒绝 "2026-09-09junk" 尾部垃圾字符与 2026-02-30 等非法日历日 - 新增 PermissionServiceOrderRowFilterTest:applyOrderRowFilter 直接单测, 覆盖总部不过滤/代理自己+下级+代管/代管物业 OR 分支/销售非负责人收窄/agentId 缺失拒绝 --- .../me/iiv/agent/mcp/tool/OrderTools.java | 67 ++++-- .../me/iiv/agent/mcp/tool/OrderToolsTest.java | 53 ++++- .../PermissionServiceOrderRowFilterTest.java | 197 ++++++++++++++++++ 3 files changed, 291 insertions(+), 26 deletions(-) create mode 100644 xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceOrderRowFilterTest.java diff --git a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/OrderTools.java b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/OrderTools.java index d9647f0..bf86d71 100644 --- a/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/OrderTools.java +++ b/xiaoqu-agent-service/src/main/java/me/iiv/agent/mcp/tool/OrderTools.java @@ -25,14 +25,18 @@ import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; -import java.text.ParseException; -import java.text.SimpleDateFormat; +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; /** @@ -44,7 +48,13 @@ public class OrderTools implements ToolObject { private static final int DEFAULT_ROWS = 20; private static final int MAX_ROWS = 100; - private static final String DATE_FORMAT = "yyyy-MM-dd"; + /** 商城业务时区固定 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; @@ -95,7 +105,7 @@ public class OrderTools implements ToolObject { @ToolParam(required = false, description = "页码,从 1 开始,默认 1") Integer page, @ToolParam(required = false, description = "每页条数,默认 20,上限 100(超出截断为 100)") Integer rows) { Date startTime = parseDate(startOrderTime, "startOrderTime"); - Date endTime = parseDate(endOrderTime, "endOrderTime"); + 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; @@ -124,8 +134,8 @@ public class OrderTools implements ToolObject { if (startTime != null) { wrapper.ge(MallGoodsOrder::getOrderTime, startTime); } - if (endTime != null) { - wrapper.le(MallGoodsOrder::getOrderTime, endOfDay(endTime)); + if (endTimeExclusive != null) { + wrapper.lt(MallGoodsOrder::getOrderTime, endTimeExclusive); } permissionService.applyOrderRowFilter(wrapper, user); @@ -142,13 +152,12 @@ public class OrderTools implements ToolObject { Map agentJinideNames = agentJinideNames(orders); Map propertyJinideNames = propertyJinideNames(orders); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); List> records = new ArrayList<>(); for (MallGoodsOrder order : orders) { Map record = new LinkedHashMap<>(); record.put("id", order.getId()); record.put("orderNo", order.getOrderNo()); - record.put("orderTime", order.getOrderTime() == null ? null : sdf.format(order.getOrderTime())); + 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()); @@ -206,11 +215,10 @@ public class OrderTools implements ToolObject { .eq(MallOrderDetails::getOrderNo, orderNo) .orderByAsc(MallOrderDetails::getId)); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Map record = new LinkedHashMap<>(); record.put("id", order.getId()); record.put("orderNo", order.getOrderNo()); - record.put("orderTime", order.getOrderTime() == null ? null : sdf.format(order.getOrderTime())); + 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()); @@ -237,11 +245,11 @@ public class OrderTools implements ToolObject { record.put("warehouseState", order.getWarehouseState()); record.put("orderInvoiceState", order.getOrderInvoiceState()); record.put("payNo", order.getPayNo()); - record.put("payTime", order.getPayTime() == null ? null : sdf.format(order.getPayTime())); - record.put("receiptTime", order.getReceiptTime() == null ? null : sdf.format(order.getReceiptTime())); - record.put("signTime", order.getSignTime() == null ? null : sdf.format(order.getSignTime())); + 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 : sdf.format(order.getCreateTime())); + 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()); @@ -302,7 +310,6 @@ public class OrderTools implements ToolObject { .last("LIMIT 1")); MallOrderLogistics logistics = logisticsList.isEmpty() ? null : logisticsList.get(0); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); if (logistics == null) { result.put("logistics", null); } else { @@ -313,7 +320,7 @@ public class OrderTools implements ToolObject { logisticsMap.put("state", logistics.getState()); logisticsMap.put("deliverName", logistics.getDeliverName()); logisticsMap.put("deliverNumber", logistics.getDeliverNumber()); - logisticsMap.put("sendTime", logistics.getModifyTime() == null ? null : sdf.format(logistics.getModifyTime())); + 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()); @@ -333,7 +340,7 @@ public class OrderTools implements ToolObject { track.put("memo", detai.getMemo()); track.put("state", detai.getState()); track.put("picList", detai.getPicList()); - track.put("createTime", detai.getCreateTime() == null ? null : sdf.format(detai.getCreateTime())); + track.put("createTime", detai.getCreateTime() == null ? null : formatTime(detai.getCreateTime())); tracks.add(track); } result.put("tracks", tracks); @@ -386,20 +393,34 @@ public class OrderTools implements ToolObject { 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; } - SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); - sdf.setLenient(false); + String trimmed = value.trim(); + if (!DATE_PATTERN.matcher(trimmed).matches()) { + throw new IllegalArgumentException(name + " 格式必须为 yyyy-MM-dd"); + } try { - return sdf.parse(value.trim()); - } catch (ParseException e) { + return LocalDate.parse(trimmed, DATE_FORMATTER); + } catch (DateTimeParseException e) { throw new IllegalArgumentException(name + " 格式必须为 yyyy-MM-dd"); } } - private static Date endOfDay(Date date) { - return new Date(date.getTime() + 24L * 3600 * 1000 - 1000); + private static String formatTime(Date date) { + return TIME_FORMATTER.format(date.toInstant()); } } diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/OrderToolsTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/OrderToolsTest.java index 0118eb7..ef02e72 100644 --- a/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/OrderToolsTest.java +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/mcp/tool/OrderToolsTest.java @@ -30,7 +30,9 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.math.BigDecimal; -import java.text.SimpleDateFormat; +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; @@ -47,6 +49,10 @@ 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 @@ -178,7 +184,7 @@ class OrderToolsTest { assertEquals("13800000000", record.get("reveiveNumber")); assertEquals("南山区", record.get("countyseat")); assertEquals("科技园 1 栋", record.get("address")); - assertEquals(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(1700000000000L)), + assertEquals(TIME_FORMATTER.format(new Date(1700000000000L).toInstant()), record.get("orderTime")); @SuppressWarnings("unchecked") List> items = (List>) result.get("items"); @@ -239,7 +245,7 @@ class OrderToolsTest { List> tracks = (List>) result.get("tracks"); assertEquals(1, tracks.size()); assertEquals("包裹已到达深圳转运中心", tracks.get(0).get("memo")); - assertEquals(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(1700000200000L)), + assertEquals(TIME_FORMATTER.format(new Date(1700000200000L).toInstant()), tracks.get(0).get("createTime")); @SuppressWarnings("unchecked") @@ -254,6 +260,47 @@ class OrderToolsTest { verify(permissionService).applyOrderRowFilter(any(), any()); } + @Test + void listAppliesDateRangeInShanghaiZoneWithExclusiveEnd() { + when(orderMapper.selectCount(any())).thenReturn(0L); + when(orderMapper.selectList(any())).thenReturn(List.of()); + + Map 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> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); + verify(orderMapper).selectList(captor.capture()); + LambdaQueryWrapper 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)); diff --git a/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceOrderRowFilterTest.java b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceOrderRowFilterTest.java new file mode 100644 index 0000000..7b36d56 --- /dev/null +++ b/xiaoqu-agent-service/src/test/java/me/iiv/agent/permission/PermissionServiceOrderRowFilterTest.java @@ -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 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 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 wrapper = new LambdaQueryWrapper<>(); + permissionService.applyOrderRowFilter(wrapper, user); + + String sql = wrapper.getSqlSegment(); + Collection 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 wrapper = new LambdaQueryWrapper<>(); + permissionService.applyOrderRowFilter(wrapper, user); + + String sql = wrapper.getSqlSegment(); + Collection 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 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 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; + } +}