rk
6 天以前 3aef471b170a703b501ddb4d9d2a12791d07ff28
代码生成
已修改79个文件
5505 ■■■■ 文件已修改
server/.gitignore 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/admin/src/main/java/com/doumee/api/business/BannerController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/admin/src/main/java/com/doumee/api/business/DriverInfoController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/admin/src/main/java/com/doumee/api/business/MemberController.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/admin/src/main/java/com/doumee/api/business/OrdersController.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/admin/src/main/java/com/doumee/api/business/PricingRuleController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/admin/src/main/java/com/doumee/api/business/RevenueController.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/admin/src/main/java/com/doumee/api/business/WithdrawalOrdersController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/db/db_change.sql 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/config/jwt/JwtTokenUtil.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/config/jwt/WebMvcConfig.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/config/wx/WxMiniUtilService.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/core/annotation/LoginShopRequired.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/core/constants/Constants.java 207 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/core/constants/ResponseStatus.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/core/utils/Tencent/MapUtil.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/core/utils/aliyun/ALiYunSmSUtil.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/Banner.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/DriverInfo.java 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/Member.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/MemberRevenue.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/Multifile.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/OrderLog.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/Orders.java 60 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/OrdersDetail.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/PricingRule.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/Revenue.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/ShopInfo.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/Smsrecord.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/WithdrawalOrders.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/AuditDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/EstimatedDeliverySaveDTO.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/OperationConfigDTO.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/ShopLoginDTO.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/UpdMobileRequest.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/AccountResponse.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/EstimatedDeliveryVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/ItemPriceVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/MemberDetailVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/OrderDetailVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/OrderDispatchVO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/OrderItemVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/PayResponse.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/ShopDetailVO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/UserCenterVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/AddrService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/AreasService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/BannerService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/CategoryService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/DriverInfoService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/MemberService.java 42 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/OrdersService.java 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/PricingRuleService.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/ShopInfoService.java 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/SmsrecordService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/WithdrawalOrdersService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/AddrServiceImpl.java 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/AreasServiceImpl.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/BannerServiceImpl.java 136 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/CategoryServiceImpl.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/DriverInfoServiceImpl.java 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/IdentityInfoServiceImpl.java 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/MemberServiceImpl.java 220 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/OrdersServiceImpl.java 1934 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/PricingRuleServiceImpl.java 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/ShopInfoServiceImpl.java 365 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/SmsrecordServiceImpl.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/WithdrawalOrdersServiceImpl.java 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/resources/application-dev.yml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/resources/application-pro.yml 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/resources/application-test.yml 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/AccountApi.java 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/AddrApi.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/ApiController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/ConfigApi.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/MemberApi.java 59 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/OrdersApi.java 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/PaymentCallback.java 121 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/ShopInfoApi.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/.gitignore
@@ -25,3 +25,5 @@
*.jar
*.war
/CLAUDE.md
/.claude/settings.local.json
/.vscode/settings.json
server/admin/src/main/java/com/doumee/api/business/BannerController.java
@@ -68,6 +68,14 @@
        return ApiResponse.success(null);
    }
    @ApiOperation("修改状态")
    @PostMapping("/updateStatus")
    @RequiresPermissions("business:banner:update")
    public ApiResponse updateStatus(@RequestBody Banner banner) {
        bannerService.updateStatus(banner);
        return ApiResponse.success(null);
    }
    @ApiOperation("分页查询")
    @PostMapping("/page")
    @RequiresPermissions("business:banner:query")
server/admin/src/main/java/com/doumee/api/business/DriverInfoController.java
@@ -8,6 +8,7 @@
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.DriverInfo;
import com.doumee.dao.dto.AuditDTO;
import com.doumee.dao.dto.ChangeStatusDTO;
import com.doumee.service.business.DriverInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -91,13 +92,28 @@
        return ApiResponse.success(driverInfoService.findById(id));
    }
    @ApiOperation("司机详情(含附件)")
    @GetMapping("/detail/{id}")
    @RequiresPermissions("business:driverInfo:query")
    public ApiResponse getDetail(@PathVariable Integer id) {
        return ApiResponse.success(driverInfoService.getDetail(id));
    }
    @ApiOperation("审批司机实名认证")
    @PostMapping("/audit")
    @RequiresPermissions("business:driverInfo:audit")
    @RequiresPermissions("business:driverInfo:update")
    public ApiResponse audit(@RequestBody AuditDTO auditDTO) {
        auditDTO.setAuditUser(this.getLoginUser().getId());
        driverInfoService.auditVerify(auditDTO);
        return ApiResponse.success("审批成功");
    }
    @ApiOperation("修改司机状态")
    @PostMapping("/changeStatus")
    @RequiresPermissions("business:driverInfo:update")
    public ApiResponse changeStatus(@RequestBody ChangeStatusDTO dto) {
        driverInfoService.changeStatus(dto);
        return ApiResponse.success("操作成功");
    }
}
server/admin/src/main/java/com/doumee/api/business/MemberController.java
@@ -8,6 +8,9 @@
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.Member;
import com.doumee.dao.dto.MemberListQueryDTO;
import com.doumee.dao.vo.MemberDetailVO;
import com.doumee.dao.vo.MemberListVO;
import com.doumee.service.business.MemberService;
import com.github.xiaoymin.knife4j.core.util.CollectionUtils;
import io.swagger.annotations.Api;
@@ -93,4 +96,26 @@
    public ApiResponse findById(@PathVariable Integer id) {
        return ApiResponse.success(memberService.findById(id));
    }
    @ApiOperation("会员列表分页查询")
    @PostMapping("/list/page")
    @RequiresPermissions("business:member:query")
    public ApiResponse<PageData<MemberListVO>> findMemberListPage(@RequestBody PageWrap<MemberListQueryDTO> pageWrap) {
        return ApiResponse.success(memberService.findMemberListPage(pageWrap));
    }
    @ApiOperation("会员列表导出Excel")
    @PostMapping("/list/exportExcel")
    @RequiresPermissions("business:member:exportExcel")
    public void exportMemberListExcel(@RequestBody PageWrap<MemberListQueryDTO> pageWrap, HttpServletResponse response) {
        List<MemberListVO> list = memberService.findMemberListPage(pageWrap).getRecords();
        ExcelExporter.build(MemberListVO.class).export(list, "会员列表", response);
    }
    @ApiOperation("会员详情查询")
    @GetMapping("/detail/{id}")
    @RequiresPermissions("business:member:query")
    public ApiResponse<MemberDetailVO> findMemberDetail(@PathVariable Integer id) {
        return ApiResponse.success(memberService.findMemberDetail(id));
    }
}
server/admin/src/main/java/com/doumee/api/business/OrdersController.java
@@ -6,6 +6,11 @@
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.Orders;
import com.doumee.dao.dto.ConfirmArriveDTO;
import com.doumee.dao.dto.DispatchDTO;
import com.doumee.dao.vo.OrderDetailVO;
import com.doumee.dao.vo.OrderDispatchVO;
import com.doumee.dao.vo.OrderSummaryVO;
import com.doumee.service.business.OrdersService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -66,6 +71,13 @@
        return ApiResponse.success(ordersService.findPage(pageWrap));
    }
    @ApiOperation("订单汇总统计")
    @PostMapping("/summary")
    @RequiresPermissions("business:orders:query")
    public ApiResponse<OrderSummaryVO> findSummary(@RequestBody PageWrap<Orders> pageWrap) {
        return ApiResponse.success(ordersService.findSummary(pageWrap));
    }
    @ApiOperation("导出Excel")
    @PostMapping("/exportExcel")
    @RequiresPermissions("business:orders:exportExcel")
@@ -80,4 +92,34 @@
        return ApiResponse.success(ordersService.findById(id));
    }
    @ApiOperation("订单详情")
    @GetMapping("/detail/{id}")
    @RequiresPermissions("business:orders:query")
    public ApiResponse<OrderDetailVO> findDetail(@PathVariable Integer id) {
        return ApiResponse.success(ordersService.findDetail(id));
    }
    @ApiOperation("手动派单信息")
    @GetMapping("/dispatch/{id}")
    @RequiresPermissions("business:orders:query")
    public ApiResponse<OrderDispatchVO> findDispatchInfo(@PathVariable Integer id) {
        return ApiResponse.success(ordersService.findDispatchInfo(id));
    }
    @ApiOperation("订单派单")
    @PostMapping("/dispatch")
    @RequiresPermissions("business:orders:update")
    public ApiResponse dispatch(@RequestBody DispatchDTO dto) {
        ordersService.dispatch(dto);
        return ApiResponse.success(null);
    }
    @ApiOperation("确认顾客到店")
    @PostMapping("/confirmArrived")
    @RequiresPermissions("business:orders:update")
    public ApiResponse confirmArrived(@RequestBody ConfirmArriveDTO dto) {
        ordersService.confirmCustomerArrived(dto.getOrderId(), dto.getShopId());
        return ApiResponse.success(null);
    }
}
server/admin/src/main/java/com/doumee/api/business/PricingRuleController.java
@@ -145,7 +145,7 @@
    @ApiOperation("查询预计时效配置")
    @GetMapping("/estimatedDelivery/list")
    @RequiresPermissions("business:pricingRule:query")
    public ApiResponse<EstimatedDeliveryVO> listEstimatedDelivery(@RequestParam Integer cityId) {
    public ApiResponse<List<EstimatedDeliveryVO>> listEstimatedDelivery(@RequestParam Integer cityId) {
        return ApiResponse.success(pricingRuleService.getEstimatedDelivery(cityId));
    }
server/admin/src/main/java/com/doumee/api/business/RevenueController.java
@@ -70,21 +70,18 @@
    @ApiOperation("分页查询")
    @PostMapping("/page")
    @RequiresPermissions("business:revenue:query")
    public ApiResponse<PageData<Revenue>> findPage(@RequestBody PageWrap<Revenue> pageWrap) {
        return ApiResponse.success(revenueService.findPage(pageWrap));
    }
    @ApiOperation("导出Excel")
    @PostMapping("/exportExcel")
    @RequiresPermissions("business:revenue:exportExcel")
    public void exportExcel(@RequestBody PageWrap<Revenue> pageWrap, HttpServletResponse response) {
        ExcelExporter.build(Revenue.class).export(revenueService.findPage(pageWrap).getRecords(), "收支记录", response);
    }
    @ApiOperation("根据ID查询")
    @GetMapping("/{id}")
    @RequiresPermissions("business:revenue:query")
    public ApiResponse findById(@PathVariable Integer id) {
        return ApiResponse.success(revenueService.findById(id));
    }
server/admin/src/main/java/com/doumee/api/business/WithdrawalOrdersController.java
@@ -6,6 +6,7 @@
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.WithdrawalOrders;
import com.doumee.dao.dto.WithdrawalApproveDTO;
import com.doumee.service.business.WithdrawalOrdersService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -76,8 +77,23 @@
    @ApiOperation("根据ID查询")
    @GetMapping("/{id}")
    @RequiresPermissions("business:withdrawalOrders:query")
    public ApiResponse findById(@PathVariable Integer id) {
    public ApiResponse<WithdrawalOrders> findById(@PathVariable Integer id) {
        return ApiResponse.success(withdrawalOrdersService.findById(id));
    }
    @ApiOperation("已提现统计金额")
    @PostMapping("/totalAmount")
    @RequiresPermissions("business:withdrawalOrders:query")
    public ApiResponse<Long> totalAmount(@RequestBody PageWrap<WithdrawalOrders> pageWrap) {
        return ApiResponse.success(withdrawalOrdersService.totalAmount(pageWrap));
    }
    @ApiOperation("提现审批")
    @PostMapping("/approve")
    @RequiresPermissions("business:withdrawalOrders:update")
    public ApiResponse approve(@RequestBody WithdrawalApproveDTO dto) {
        withdrawalOrdersService.approve(dto);
        return ApiResponse.success(null);
    }
}
server/services/db/db_change.sql
@@ -5,10 +5,91 @@
-- ============================================================
-- 2026/04/15 订单表添加核销码字段
-- ============================================================
ALTER TABLE `orders` ADD COLUMN `MEMBER_VERIFY_CODE` VARCHAR(32) DEFAULT NULL COMMENT '会员订单核销码' AFTER `OVERDUE_STATUS`;
ALTER TABLE `orders` ADD COLUMN `DRIVER_VERIFY_CODE` VARCHAR(32) DEFAULT NULL COMMENT '司机订单核销码' AFTER `MEMBER_VERIFY_CODE`;
-- ============================================================
-- 2026/04/15 订单表添加是否逾期字段
-- ============================================================
ALTER TABLE `orders` ADD COLUMN `OVERDUE_STATUS` TINYINT DEFAULT 0 COMMENT '是否逾期: 0=否 1=是 2=已支付' AFTER `OVERDUE_DAYS`;
-- ============================================================
-- 2026/04/15 订单物品明细表添加就地存取单价字段
-- ============================================================
ALTER TABLE `orders_detail` ADD COLUMN `LOCALLY_PRICE` BIGINT DEFAULT NULL COMMENT '就地存取单价(分/天)' AFTER `EXTRA_PRICE`;
-- ============================================================
-- 2026/04/14 门店营业类型字段
-- ============================================================
ALTER TABLE `shop_info` ADD COLUMN `BUSINESS_TYPE` INT DEFAULT 0 COMMENT '门店营业类型:0=非全天;1=全天' AFTER `SHOP_HOURS`;
-- ============================================================
-- 2026/04/14 门店信息维护字段
-- ============================================================
ALTER TABLE `shop_info` ADD COLUMN `COVER_IMG` VARCHAR(500) DEFAULT NULL COMMENT '门店头像' AFTER `PAY_MEMBER_OPEN_ID`;
ALTER TABLE `shop_info` ADD COLUMN `CONTENT` TEXT DEFAULT NULL COMMENT '门店介绍' AFTER `COVER_IMG`;
ALTER TABLE `shop_info` ADD COLUMN `DEPOSIT_TYPES` VARCHAR(500) DEFAULT NULL COMMENT '寄存类型(逗号分隔的category主键)' AFTER `CONTENT`;
ALTER TABLE `shop_info` ADD COLUMN `FEE_STANDARD` VARCHAR(500) DEFAULT NULL COMMENT '收费标准' AFTER `DEPOSIT_TYPES`;
ALTER TABLE `shop_info` ADD COLUMN `DELIVERY_AREA` DECIMAL(10,2) DEFAULT NULL COMMENT '配送范围(km)' AFTER `FEE_STANDARD`;
ALTER TABLE `shop_info` ADD COLUMN `SHOP_HOURS` VARCHAR(100) DEFAULT NULL COMMENT '营业时间' AFTER `DELIVERY_AREA`;
-- ============================================================
-- 2026/04/13 提现申请用户类型字段
-- ============================================================
ALTER TABLE `withdrawal_orders` ADD COLUMN `MEMBER_TYPE` INT DEFAULT NULL COMMENT '用户类型:1=司机;2=店铺' AFTER `MEMBER_ID`;
-- ============================================================
-- 2026/04/13 提现申请审批字段 + 审批权限
-- ============================================================
ALTER TABLE `withdrawal_orders` ADD COLUMN `USER_ID` INT DEFAULT NULL COMMENT '审批操作人(关联system_user)' AFTER `ALI_ACCOUNT`;
ALTER TABLE `withdrawal_orders` ADD COLUMN `APPROVE_TIME` DATETIME DEFAULT NULL COMMENT '审批时间' AFTER `USER_ID`;
ALTER TABLE `withdrawal_orders` ADD COLUMN `APPROVE_REMARK` VARCHAR(500) DEFAULT NULL COMMENT '审批备注' AFTER `APPROVE_TIME`;
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `MODULE`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:withdrawalOrders:create', '新建提现申请', '提现申请', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `MODULE`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:withdrawalOrders:delete', '删除提现申请', '提现申请', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `MODULE`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:withdrawalOrders:update', '修改提现申请', '提现申请', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `MODULE`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:withdrawalOrders:query', '查询提现申请', '提现申请', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `MODULE`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:withdrawalOrders:exportExcel', '导出提现申请(Excel)', '提现申请', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
-- ============================================================
-- 2026/04/13 订单退款记录权限
-- ============================================================
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `MODULE`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ordersRefund:create', '新建订单退款记录', '订单退款记录', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `MODULE`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ordersRefund:delete', '删除订单退款记录', '订单退款记录', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `MODULE`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ordersRefund:update', '修改订单退款记录', '订单退款记录', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `MODULE`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:ordersRefund:query', '查询订单退款记录', '订单退款记录', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
-- ============================================================
-- 2026/04/13 会员列表与详情权限
-- ============================================================
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `MODULE`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:member:list', '查询会员列表', '会员管理', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
INSERT INTO `SYSTEM_PERMISSION`(`CODE`, `NAME`, `MODULE`, `REMARK`, `FIXED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`) VALUES ('business:member:listExport', '导出会员列表(Excel)', '会员管理', '', 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0);
-- ============================================================
-- 2026/04/13 订单新增取消备注字段 + 订单文件路径字典
-- ============================================================
ALTER TABLE `orders` ADD COLUMN `cancel_remark` VARCHAR(500) DEFAULT NULL COMMENT '取消备注' AFTER `cancel_time`;
ALTER TABLE `orders` ADD COLUMN `CANCEL_REMARK` VARCHAR(500) DEFAULT NULL COMMENT '取消备注' AFTER `CANCEL_TIME`;
INSERT INTO `system_dict_data`(`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `UPDATE_USER`, `UPDATE_TIME`, `DELETED`)
SELECT 100, '/orders/', 'ORDERS_FILES', '订单文件路径', 22, 0, 1, CURRENT_TIMESTAMP, NULL, NULL, 0
@@ -69,15 +150,15 @@
-- 2026/04/13 支付宝提现账户字段
-- ============================================================
ALTER TABLE `withdrawal_orders` ADD COLUMN `ali_account` VARCHAR(64) DEFAULT NULL COMMENT '支付宝提现账户' AFTER `out_bill_no`;
ALTER TABLE `shop_info` ADD COLUMN `ali_account` VARCHAR(64) DEFAULT NULL COMMENT '支付宝提现账户' AFTER `delivery_area`;
ALTER TABLE `withdrawal_orders` ADD COLUMN `ALI_ACCOUNT` VARCHAR(64) DEFAULT NULL COMMENT '支付宝提现账户' AFTER `OUT_BILL_NO`;
ALTER TABLE `shop_info` ADD COLUMN `ALI_ACCOUNT` VARCHAR(64) DEFAULT NULL COMMENT '支付宝提现账户' AFTER `DELIVERY_AREA`;
-- ============================================================
-- 2026/04/13 轮播图业务调整
-- ============================================================
ALTER TABLE `banner` MODIFY COLUMN `title` VARCHAR(255) DEFAULT NULL COMMENT '标题';
ALTER TABLE `banner` MODIFY COLUMN `TITLE` VARCHAR(255) DEFAULT NULL COMMENT '标题';
INSERT INTO `SYSTEM_DICT_DATA`(`TYPE`, `CODE`, `NAME`, `VALUE`, `REMARK`, `CREATE_TIME`, `DELETED`)
VALUES ('SYSTEM', 'BANNER_FILES', '轮播图文件路径', '/banner/', '轮播图图片存储子路径', CURRENT_TIMESTAMP, 0);
server/services/src/main/java/com/doumee/config/jwt/JwtTokenUtil.java
@@ -31,6 +31,10 @@
    public static final String MEMBER_INFO = "MEMBER_INFO";
    public static final String SHOP_ID = "SHOP_ID";
    public static final String SHOP_INFO = "SHOP_INFO";
    @Resource
    private RedisTemplate<String,Object> redisTemplate;
server/services/src/main/java/com/doumee/config/jwt/WebMvcConfig.java
@@ -2,15 +2,18 @@
import com.alibaba.fastjson.JSONObject;
import com.doumee.core.annotation.LoginRequired;
import com.doumee.core.annotation.LoginShopRequired;
import com.doumee.core.constants.Constants;
import com.doumee.core.constants.ResponseStatus;
import com.doumee.core.exception.BusinessException;
import com.doumee.dao.business.model.Member;
import com.doumee.dao.business.model.ShopInfo;
import io.jsonwebtoken.JwtException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.method.HandlerMethod;
@@ -30,8 +33,9 @@
    @Autowired
    private JdbcTemplate dao;
    @Resource
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    /**
     * 添加拦截器
@@ -55,7 +59,7 @@
                    //获取token
                    String token = request.getHeader(JwtTokenUtil.HEADER_KEY);  // 从 http 请求头中取出 token
                    if (StringUtils.isNotBlank(token)) {
                        checkLogin(request,response);
                        checkMemberLogin(request,response);
                    } else {
                        throw new BusinessException(ResponseStatus.BE_OVERDUE.getCode(),"未登录");
                    }
@@ -63,7 +67,23 @@
                    //获取token
                    String token = request.getHeader(JwtTokenUtil.HEADER_KEY);  // 从 http 请求头中取出 token
                    if (StringUtils.isNotBlank(token)) {
                        checkLogin(request,response);
                        checkMemberLogin(request,response);
                    } else {
                        throw new BusinessException(ResponseStatus.BE_OVERDUE.getCode(),"未登录");
                    }
                }else if (beanType.isAnnotationPresent(LoginShopRequired.class)) {
                    //获取token
                    String token = request.getHeader(JwtTokenUtil.HEADER_KEY);  // 从 http 请求头中取出 token
                    if (StringUtils.isNotBlank(token)) {
                        checkShopLogin(request,response);
                    } else {
                        throw new BusinessException(ResponseStatus.BE_OVERDUE.getCode(),"未登录");
                    }
                }else if (handlerMethod.hasMethodAnnotation(LoginShopRequired.class)){
                    //获取token
                    String token = request.getHeader(JwtTokenUtil.HEADER_KEY);  // 从 http 请求头中取出 token
                    if (StringUtils.isNotBlank(token)) {
                        checkShopLogin(request,response);
                    } else {
                        throw new BusinessException(ResponseStatus.BE_OVERDUE.getCode(),"未登录");
                    }
@@ -76,12 +96,20 @@
    public Boolean checkLogin(HttpServletRequest request, HttpServletResponse response){
    public Boolean checkMemberLogin(HttpServletRequest request, HttpServletResponse response){
        String token = request.getHeader(JwtTokenUtil.HEADER_KEY);
        try {
            Member member  = jwtTokenUtil.getUserInfoByToken(token);
            if(!token.startsWith(Constants.ZERO+"")){
                throw new BusinessException(ResponseStatus.TOKEN_EXCEED_TIME);
            }
            String tokenRedis = (String) redisTemplate.opsForValue().get(token);
            if(StringUtils.isBlank(tokenRedis)){
                throw new BusinessException(ResponseStatus.BE_OVERDUE);
            }
            Member member = JSONObject.parseObject(tokenRedis, Member.class);
            if(Objects.isNull(member)){
                throw new BusinessException(ResponseStatus.TOKEN_EXCEED_TIME.getCode(),"长时间未操作,请重新登录");
                throw new BusinessException(ResponseStatus.TOKEN_EXCEED_TIME);
            }
            Integer isDeleted = dao.queryForObject(" select COALESCE(DELETED,1)  from member where id  = ?", Integer.class, member.getId());
            if(isDeleted.equals(Constants.ONE)){
@@ -89,7 +117,6 @@
            }
            Integer count = dao.queryForObject("select count(1) from member where id  = ?", Integer.class, member.getId());
            if (count != null && count > 0) {
//                jwtTokenUtil.refreshToken(token,member);
                request.setAttribute(JwtTokenUtil.MEMBER_INFO, JSONObject.toJSONString(member));
                request.setAttribute(JwtTokenUtil.MEMBER_ID, member.getId());
                return true;
@@ -101,6 +128,59 @@
        }
    }
    public Boolean checkShopLogin(HttpServletRequest request, HttpServletResponse response){
        String token = request.getHeader(JwtTokenUtil.HEADER_KEY);
        try {
            if(!token.startsWith(Constants.TWO+"")){
                throw new BusinessException(ResponseStatus.TOKEN_EXCEED_TIME);
            }
            String tokenRedis = (String) redisTemplate.opsForValue().get(token);
            if(StringUtils.isBlank(tokenRedis)){
                throw new BusinessException(ResponseStatus.BE_OVERDUE);
            }
            ShopInfo shop = JSONObject.parseObject(tokenRedis, ShopInfo.class);
            if(Objects.isNull(shop)){
                throw new BusinessException(ResponseStatus.BE_OVERDUE);
            }
            String openid = shop.getOpenid();
            Integer shopId = getTokenId(token);
            Integer isDeleted = dao.queryForObject(" select COALESCE(ISDELETED,0)  from shop_info where id  = ?", Integer.class, shopId);
            if(isDeleted== Constants.ONE){
                throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"商户已删除,请联系管理员");
            }
            Integer isForbidden = dao.queryForObject(" select COALESCE(STATUS,0)  from shop_info where id  = ?", Integer.class, shopId);
            if(isForbidden == Constants.ONE){
                throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"商户已禁用,请联系管理员");
            }
            String dbOpenid = dao.queryForObject(" select ifnull(openid,'')  from shop where id  = ?", String.class, shopId);
            if(StringUtils.isBlank(dbOpenid)||!openid.equals(dbOpenid)){
                throw new BusinessException(ResponseStatus.TOKEN_EXCEED_TIME);
            }
            Integer count = dao.queryForObject("select count(1) from shop where id  = ?", Integer.class, shopId);
            if (count != null && count > 0) {
                request.setAttribute(JwtTokenUtil.SHOP_INFO, JSONObject.toJSONString(shop));
                request.setAttribute(JwtTokenUtil.SHOP_ID, shop.getId());
                return true;
            }else{
                throw new BusinessException(ResponseStatus.BE_OVERDUE.getCode(),"用户信息出错");
            }
        } catch (IllegalArgumentException | JwtException e) {
            throw new BusinessException(ResponseStatus.BE_OVERDUE);
        }
    }
    public Integer getTokenId(String token){
        try {
            Integer lastIndex = token.lastIndexOf("_")+1;
            Integer tokenId = Integer.valueOf(token.substring(lastIndex));
            return tokenId;
        }catch (Exception e){
            throw new BusinessException(ResponseStatus.BE_OVERDUE);
        }
    }
    @Bean
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
server/services/src/main/java/com/doumee/config/wx/WxMiniUtilService.java
@@ -1,26 +1,16 @@
package com.doumee.core.wx;
package com.doumee.config.wx;
import cn.binarywang.wx.miniapp.api.WxMaQrcodeService;
import com.doumee.core.constants.ResponseStatus;
import com.doumee.core.exception.BusinessException;
import com.doumee.core.utils.ID;
import com.github.binarywang.wxpay.bean.request.BaseWxPayRequest;
import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.Map;
/**
 * 微信小程序-公共方法
@@ -37,17 +27,15 @@
     * refundPrice;退款金额
     */
    @Transactional(rollbackFor = Exception.class)
    public static String wxRefund(String orderNo, BigDecimal totalPrice, BigDecimal refundPrice) {
    public String wxRefund(String orderNo, Long totalPrice, Long refundPrice) {
        try {
            // 发送退款请求
            String refNum = ID.nextGUID();
            WxPayRefundRequest request = new WxPayRefundRequest();
            request.setOutTradeNo(orderNo);
            request.setOutRefundNo(refNum);
           // request.setTotalFee(2);
          //  request.setRefundFee(1);
            request.setTotalFee(1);//BaseWxPayRequest.yuanToFen(totalPrice.toString()));
            request.setRefundFee(1);//BaseWxPayRequest.yuanToFen(refundPrice.toString()));
            request.setTotalFee(totalPrice.intValue());
            request.setRefundFee(refundPrice.intValue());
            WxPayRefundResult response = WxMiniConfig.wxPayService.refund(request);
            if ("SUCCESS".equals(response.getReturnCode()) && "SUCCESS".equals(response.getResultCode())) {
                return refNum;
server/services/src/main/java/com/doumee/core/annotation/LoginShopRequired.java
@@ -1,4 +1,4 @@
package com.doumee.config.annotation;
package com.doumee.core.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
server/services/src/main/java/com/doumee/core/constants/Constants.java
@@ -66,6 +66,20 @@
    public static final String MEMBER_FILES = "MEMBER_FILES";
    public static final String CATEGORY_FILES = "CATEGORY_FILES";
    public static final String SHOP_FILES = "SHOP_FILES";
    public static final String DRIVER_FILES = "DRIVER_FILES";
    public static final String BANNER_FILES = "BANNER_FILES";
    public static final String ORDERS_FILES = "ORDERS_FILES";
    // 运营配置
    public static final String OPERATION_CONFIG = "OPERATION_CONFIG";
    public static final String OP_DRIVER_DAILY_CANCEL_LIMIT = "DRIVER_DAILY_CANCEL_LIMIT";
    public static final String OP_UNPICKED_DISCOUNT = "UNPICKED_DISCOUNT";
    public static final String OP_SETTLEMENT_DATE = "SETTLEMENT_DATE";
    public static final String OP_URGENT_COEFFICIENT = "URGENT_COEFFICIENT";
    public static final String OP_AUTO_CANCEL_TIME = "AUTO_CANCEL_TIME";
    public static final String OP_INSURANCE_RATE = "INSURANCE_RATE";
    public static final String OP_ORDER_ACCEPT_LIMIT = "ORDER_ACCEPT_LIMIT";
    public static final String OP_AUTO_CONFIRM_RECEIPT = "AUTO_CONFIRM_RECEIPT";
    public static boolean WORKORDER_SHE_EMAIL_SENDING = false;
    public static  boolean DEALING_COMPANY_SYNC = false ;
    public static  boolean DEALING_MEMBER_SYNC = false ;
@@ -73,6 +87,18 @@
    public static final String WORKORDER_LOG_FILE_PATH ="WORKORDER_LOG_FILE_PATH" ;
    public static final String REDIS_TOKEN_KEY = "token_";
    public static final String REDIS_VERIFY_CODE_KEY = "verify_code_";
    public final static String GOODS_ORDER_CREATE_LOCK = "goods:order:create:lock:";
    // 订单日志操作类型
    public static final int ORDER_LOG_DISPATCH = 1;         // 派单
    public static final int ORDER_LOG_URGENT_FEE = 2;       // 加急费
    public static final int ORDER_LOG_ASSIGN_DRIVER = 3;    // 指派司机
    public static final int ORDER_LOG_CANCEL = 4;           // 取消订单
    public static final int ORDER_LOG_CONFIRM_ARRIVE = 5;   // 确认顾客到店
    public static final String SUCCESS = "SUCCESS";
    public static final String FAIL = "FAIL";
@@ -247,26 +273,6 @@
    }
    /**
     * 文件附件类型
     */
    public interface multiFileType{
        //0问题上报 1跌绊滑上报 2跌绊滑处理 3分配物业主管 4分配处理人 5DCA风险上报 6DCA风险处理 7DCA问题工单图片
//        int sheUpload = 0;
//        int dbhUpload = 1;
//        int dbhDeal = 2;
//        int dbhAllocation = 3;
//        int dbhDealUser = 4;
//        int dcaUpload = 5;
//        int dcaDeal = 6;
//        int dcaWorkOrder= 7;
        //0工单上报 1工单处理 2工单转交
        int  upload = 0;
        int deal = 1;
        int passOn = 2;
    }
    public enum WorkOrderStatus{
        waitConfirm( 0, "待分配WTS","{title}上报","","待分配WTS" ),
@@ -368,7 +374,7 @@
        OTHER_MATERIAL(0, "门店其他材料"),
        STORE_INTERIOR(1, "门店内部照片"),
        ORDER_DEPOSIT(2, "订单寄存图片"),
        ORDER_TAKE(3, "订单取件图片"),
        ORDER_TAKE(3, "门店入库图片"),
        DRIVER_TAKE(4, "司机取件图片"),
        DRIVER_DONE(5, "司机完成图片"),
        DRIVER_CAR(6, "司机实名认证车辆照片"),
@@ -377,15 +383,130 @@
        STORE_FRONT(9, "门店门头照"),
        SOCIAL_SECURITY(10, "社保缴纳证明"),
        LABOR_CONTRACT(11, "有效劳动合同"),
        ORDER_FILE(12,"下单图片"),
        STORE_OUT(13,"门店出库图片"),
        REFUND_TAKE(14,"退款取件图片")
        ;
        private final int key;
        private final String name;
    }
    @Getter
    @AllArgsConstructor
    public enum OrderLogType {
        urgent(0, "平台加急", "平台加急,奖励金 {param} 元。"),
        dispatch(1, "平台指派", "平台指派司机 {param} 接单。")
        ;
        private int status;
        private String title;
        private String statusInfo;
    }
    public  static String getIpAddr() {
    /**
     * 订单支付类型
     */
    @Getter
    @AllArgsConstructor
    public enum OrdersAttach {
        STORAGE_ORDER("storageOrder", "寄存订单"),
        SHOP_DEPOSIT("shopDeposit", "店铺押金订单"),
        DRIVER_DEPOSIT("driverDeposit", "司机押金订单"),
        OVERDUE_FEE("overdueFee", "逾期费用订单")
        ;
        private final String key;
        private final String name;
    }
    /**
     * 订单状态(就地/异地统一)
     * 就地寄存: 0→1→2→3→(6)→7 / 96~99
     * 异地寄存: 0→1→2→3→4→5→(6)→7 / 96~99
     */
    @Getter
    @AllArgsConstructor
    public enum OrderStatus {
        waitPay(0, "待支付"),
        waitDeposit(1, "待寄存"),
        deposited(2, "已寄存"),
        accepted(3, "已接单"),
        delivering(4, "派送中"),
        arrived(5, "已到店/已送达/待取件"),
        overdue(6, "存在逾期"), //弃用
        finished(7, "已完成"),
        closed(96, "订单关闭(退款)"),
        cancelOverdue(97, "取消逾期"), //弃用
        cancelling(98, "取消中"),
        cancelled(99, "已取消")
        ;
        private final int status;
        private final String desc;
        public int getKey() { return status; }
        public String getValue() { return desc; }
        public static OrderStatus getByKey(int index) {
            for (OrderStatus c : OrderStatus.values()) {
                if (c.getKey() == index) {
                    return c;
                }
            }
            return null;
        }
        public static String getDescByKey(int index) {
            for (OrderStatus c : OrderStatus.values()) {
                if (c.getKey() == index) {
                    return c.getValue();
                }
            }
            return "";
        }
    }
    /**
     * 会员端合并订单状态(用于分页筛选)
     */
    @Getter
    @AllArgsConstructor
    public enum OrderCombinedStatus {
        waitPay(0, "待支付", new int[]{OrderStatus.waitPay.status}),
        waitDeposit(1, "待核验", new int[]{OrderStatus.waitDeposit.status}),
        waitDeliver(2, "待配送", new int[]{OrderStatus.deposited.status}),
        waitReceive(3, "待收货", new int[]{OrderStatus.accepted.status, OrderStatus.delivering.status, OrderStatus.arrived.status}),
        finished(4, "已完成", new int[]{OrderStatus.finished.status}),
        refund(5, "退款", new int[]{OrderStatus.closed.status, OrderStatus.cancelling.status})
        ;
        private final int key;
        private final String desc;
        private final int[] statuses;
        public static OrderCombinedStatus getByKey(int key) {
            for (OrderCombinedStatus c : OrderCombinedStatus.values()) {
                if (c.getKey() == key) {
                    return c;
                }
            }
            return null;
        }
    }
    /**
     * 得到request对象
     *
     * @return
     */
    public static HttpServletRequest getRequest() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        return request;
    }
    public static String getIpAddr() {
        HttpServletRequest request = Constants.getRequest();
        String ipAddress = null;
        try {
            ipAddress = request.getHeader("x-forwarded-for");
@@ -424,4 +545,46 @@
    }
    /**
     * 根据身份证号码获取性别
     * @param idCard 身份证号码(15位或18位)
     * @return 1-男,2-女,null-无法判断
     */
    public static Integer getGenderByIdCard(String idCard) {
        if (idCard == null || (idCard.length() != 15 && idCard.length() != 18)) {
            return null;
        }
        // 15位身份证:最后一位为性别码;18位身份证:第17位(索引16)为性别码
        int genderIndex = idCard.length() == 15 ? 14 : 16;
        int genderCode = Character.getNumericValue(idCard.charAt(genderIndex));
        return genderCode % 2 == 1 ? 1 : 2;
    }
    public static BigDecimal formatBigdecimal4Float(BigDecimal d) {
        if (d == null) {
            d = new BigDecimal(0.0);
        }
        //保留两位小数且四舍五入
        d = d.setScale(4, BigDecimal.ROUND_HALF_UP);
        return  d;
    }
    /**
     * 司机定级:5=S 4=A 3=B 2=C 1=D
     *
     * @param level 等级 1-5
     * @return 等级文本
     */
    public static String getDriverLevelName(Integer level) {
        if (level == null) return null;
        switch (level) {
            case 5: return "S";
            case 4: return "A";
            case 3: return "B";
            case 2: return "C";
            case 1: return "D";
            default: return null;
        }
    }
}
server/services/src/main/java/com/doumee/core/constants/ResponseStatus.java
@@ -29,7 +29,7 @@
    MASSIVE_REQUEST(5101, "请求过于频繁"),
    NOT_ALLOWED(5110, "不允许的操作"),
    BE_OVERDUE(5112, "未登录"),
    TOKEN_EXCEED_TIME(5113, "登陆已过期"),
    TOKEN_EXCEED_TIME(5113, "对不起,登录已失效!"),
    ;
    private int code;
server/services/src/main/java/com/doumee/core/utils/Tencent/MapUtil.java
@@ -1,6 +1,124 @@
package com.doumee.core.utils.Tencent;/**
* Created by IntelliJ IDEA.
* @Author : Rk
* @create 2026/4/14 15:58
*/public class MapUtil {
package com.doumee.core.utils.Tencent;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.doumee.core.utils.Http;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
 * 腾讯地图工具类
 *
 * @Author : Rk
 * @create 2026/4/14 15:58
 */
@Slf4j
@Component
public class MapUtil {
    private static String tencentKey;
    /** 距离矩阵API */
    public static final String MATRIX_URL = "https://apis.map.qq.com/ws/distance/v1/matrix";
    /** 支持的模式 */
    private static final List<String> SUPPORTED_MODES = Arrays.asList("driving", "bicycling");
    @Value("${tencent_key}")
    public void setTencentKey(String tencentKey) {
        MapUtil.tencentKey = tencentKey;
    }
    /**
     * 批量距离矩阵计算
     *
     * @param mode       模式:driving(驾车)、bicycling(自行车)
     * @param fromPoints 起点列表,格式:lat,lng(最多10个)
     * @param toPoints   终点列表,格式:lat,lng(最多20个)
     * @return result.rows 矩阵数据,每个元素包含 distance(米) 和 duration(秒)
     */
    public static JSONObject distanceMatrix(String mode, List<String> fromPoints, List<String> toPoints) {
        if (!SUPPORTED_MODES.contains(mode)) {
            throw new IllegalArgumentException("不支持的模式: " + mode + ",仅支持: " + SUPPORTED_MODES);
        }
        if (fromPoints == null || fromPoints.isEmpty() || toPoints == null || toPoints.isEmpty()) {
            throw new IllegalArgumentException("起点和终点列表不能为空");
        }
        String from = String.join(";", fromPoints);
        String to = String.join(";", toPoints);
        try {
            String url = MATRIX_URL
                    + "?key=" + tencentKey
                    + "&mode=" + mode
                    + "&from=" + URLEncoder.encode(from, "UTF-8")
                    + "&to=" + URLEncoder.encode(to, "UTF-8");
            log.info("腾讯地图矩阵API请求: mode={}, from={}, to={}", mode, from, to);
            JSONObject json = new Http().build(url)
                    .setConnectTimeout(5000)
                    .setReadTimeout(10000)
                    .get()
                    .toJSONObject();
            log.info("腾讯地图矩阵API响应: {}", json);
            if (json.getIntValue("status") != 0) {
                throw new RuntimeException("腾讯地图矩阵API调用失败: " + json.getString("message"));
            }
            return json.getJSONObject("result");
        } catch (IOException e) {
            log.error("腾讯地图矩阵API调用异常", e);
            throw new RuntimeException("腾讯地图矩阵API调用异常", e);
        }
    }
    /**
     * 单对距离计算(便捷方法)
     *
     * @param mode 模式:driving(驾车)、bicycling(自行车)
     * @param from 起点格式:lat,lng
     * @param to   终点格式:lat,lng
     * @return 第一个元素的 distance(米) 和 duration(秒)
     */
    public static JSONObject distanceSingle(String mode, String from, String to) {
        JSONObject result = distanceMatrix(mode,
                Arrays.asList(from),
                Arrays.asList(to));
        JSONArray rows = result.getJSONArray("rows");
        if (rows != null && !rows.isEmpty()) {
            JSONArray elements = rows.getJSONObject(0).getJSONArray("elements");
            if (elements != null && !elements.isEmpty()) {
                return elements.getJSONObject(0);
            }
        }
        return new JSONObject();
    }
    /**
     * 多起点到单终点(便捷方法)
     * 返回每个起点到终点的距离和耗时,顺序与fromPoints对应
     *
     * @param mode       模式
     * @param fromPoints 起点列表
     * @param to         单个终点
     * @return 距离耗时列表,顺序与fromPoints对应
     */
    public static List<JSONObject> distanceToOne(String mode, List<String> fromPoints, String to) {
        JSONObject result = distanceMatrix(mode, fromPoints, Arrays.asList(to));
        JSONArray rows = result.getJSONArray("rows");
        return rows == null ? Arrays.asList() : rows.stream()
                .map(row -> ((JSONObject) row).getJSONArray("elements").getJSONObject(0))
                .collect(Collectors.toList());
    }
}
server/services/src/main/java/com/doumee/core/utils/aliyun/ALiYunSmSUtil.java
@@ -9,9 +9,7 @@
import com.aliyuncs.profile.DefaultProfile;
import com.doumee.core.constants.ResponseStatus;
import com.doumee.core.exception.BusinessException;
import com.doumee.core.utils.Constants;
import java.security.SecureRandom;
import java.util.Map;
/**
@@ -42,9 +40,9 @@
        // 手机号
        request.putQueryParameter("PhoneNumbers", phone);
        // 短信签名
        request.putQueryParameter("SignName", "合肥鼎元旋压科技");
        request.putQueryParameter("SignName", "");//合肥鼎元旋压科技
        // 短信模版CODE
        request.putQueryParameter("TemplateCode", "SMS_332555204");
        request.putQueryParameter("TemplateCode", "");//SMS_332555204
        // 构建短信验证码
        request.putQueryParameter("TemplateParam", JSONObject.toJSONString(codeMap));
        try {
server/services/src/main/java/com/doumee/dao/business/model/Banner.java
@@ -1,6 +1,7 @@
package com.doumee.dao.business.model;
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 com.doumee.core.annotation.excel.ExcelColumn;
@@ -45,8 +46,8 @@
    @ApiModelProperty(value = "备注")
    private String remark;
    @ApiModelProperty(value = "活动主键")
    private Integer title;
    @ApiModelProperty(value = "标题")
    private String title;
    @ApiModelProperty(value = "排序号")
    private Integer sortnum;
@@ -58,12 +59,12 @@
    @ApiModelProperty(value = "列表图")
    private String imgurl;
    @ApiModelProperty(value = "跳转类型 0富文本 1外链", example = "0")
    @ExcelColumn(name = "跳转类型", index = 3, width = 10, valueMapping = "0=富文本;1=外链;")
    @ApiModelProperty(value = "跳转类型 0无 1富文本 2外链", example = "0")
    @ExcelColumn(name = "跳转类型", index = 3, width = 10, valueMapping = "0无 1富文本 2外链")
    private Integer type;
    @ApiModelProperty(value = "位置 0首页 1店铺首页", example = "0")
    @ExcelColumn(name = "位置", index = 4, width = 10, valueMapping = "0=首页;1=店铺首页;")
    @ApiModelProperty(value = "位置 0会员端首页轮播 1司机APP引导页", example = "0")
    @ExcelColumn(name = "位置", index = 4, width = 10, valueMapping = "位置 0会员端首页轮播 1司机APP引导页")
    private Integer position;
    @ApiModelProperty(value = "店铺主键")
@@ -71,4 +72,8 @@
    @ApiModelProperty(value = "内容")
    private String content;
    @TableField(exist = false)
    @ApiModelProperty(value = "图片完整路径")
    private String imgurlFull;
}
server/services/src/main/java/com/doumee/dao/business/model/DriverInfo.java
@@ -35,7 +35,7 @@
    private Integer createUser;
    @ApiModelProperty(value = "创建时间")
    @ExcelColumn(name = "创建时间", index = 1, width = 16, dateFormat = "yyyy-MM-dd HH:mm:ss")
    @ExcelColumn(name = "创建时间", index = 7, width = 18, dateFormat = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    @ApiModelProperty(value = "更新人编码", example = "1")
@@ -49,19 +49,23 @@
    private String remark;
    @ApiModelProperty(value = "姓名")
    @ExcelColumn(name = "姓名", index = 2, width = 10)
    @ExcelColumn(name = "司机姓名", index = 1, width = 10)
    private String name;
    @ApiModelProperty(value = "手机号")
    @ExcelColumn(name = "手机号", index = 3, width = 12)
    private String telephone;
    @ApiModelProperty(value = "性别:1=男;2=女", example = "1")
    @TableField(exist = false)
    @ExcelColumn(name = "性别", index = 2, width = 6, valueMapping = "1=男;2=女")
    private Integer gender;
    @ApiModelProperty(value = "身份证号码")
    @ExcelColumn(name = "身份证号码", index = 4, width = 18)
    private String idcard;
    @ApiModelProperty(value = "婚姻状态:0=未婚;1=已婚;2=离异;3=丧偶", example = "0")
    @ExcelColumn(name = "婚姻状态", index = 5, width = 10, valueMapping = "0=未婚;1=已婚;2=离异;3=丧偶;")
    private Integer maritalStatus;
    @ApiModelProperty(value = "区划主键", example = "1")
@@ -77,7 +81,7 @@
    private Integer carType;
    @ApiModelProperty(value = "车牌号")
    @ExcelColumn(name = "车牌号", index = 6, width = 10)
    @ExcelColumn(name = "车牌号", index = 6, width = 12)
    private String carCode;
    @ApiModelProperty(value = "车辆颜色")
@@ -91,9 +95,13 @@
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date cardEndDate;
    @ApiModelProperty(value = "司机状态:0=注册;1=待审批;2=审批通过;3=审批驳回", example = "0")
    @ExcelColumn(name = "状态", index = 7, width = 10, valueMapping = "0=注册;1=待审批;2=审批通过;3=审批驳回;")
    @ApiModelProperty(value = "司机状态:0=启用;1=禁用;", example = "0")
    @ExcelColumn(name = "状态", index = 8, width = 8, valueMapping = "0=启用;1=禁用;")
    private Integer status;
    @ApiModelProperty(value = "审批状态:0=待审批;1=审批通过;2=审批驳回", example = "0")
    @ExcelColumn(name = "审批状态", index = 9, width = 10, valueMapping = "0=待审批;1=审批通过;2=审批驳回")
    private Integer auditStatus;
    @ApiModelProperty(value = "OPENID(APP)")
    private String openid;
@@ -120,6 +128,9 @@
    @ApiModelProperty(value = "身份证反面照")
    private String idcardImgBack;
    @ApiModelProperty(value = "司机定级:( 5 - 1 S A B C D )", example = "1")
    private Integer driverLevel;
    @ApiModelProperty(value = "车辆照片列表")
    @TableField(exist = false)
    private List<Multifile> carImgList = new ArrayList<>();
@@ -140,6 +151,31 @@
    @TableField(exist = false)
    private String carTypeName;
    @TableField(exist = false)
    @ApiModelProperty(value = "状态列表(查询用)", example = "0,1,2")
    private List<Integer> statusList;
    @TableField(exist = false)
    @ApiModelProperty(value = "当前余额(单位:分)", example = "10000")
    @ExcelColumn(name = "账户余额", index = 5, width = 12)
    private Long memberAmount;
    @TableField(exist = false)
    @ApiModelProperty(value = "图片前缀地址")
    private String imgPrefix;
    @TableField(exist = false)
    @ApiModelProperty(value = "查询关键字(司机姓名/手机号)")
    private String keyword;
    @TableField(exist = false)
    @ApiModelProperty(value = "创建开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date createTimeStart;
    @TableField(exist = false)
    @ApiModelProperty(value = "创建结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date createTimeEnd;
}
server/services/src/main/java/com/doumee/dao/business/model/Member.java
@@ -67,15 +67,6 @@
    @ExcelColumn(name="真实姓名",index = 4,width = 10)
    private String name;
    @ApiModelProperty(value = "用工身份:0=未申请;1=申请中;2=已通过;3=未通过;", example = "1") //弃用
    private Integer workerIdentity;
    @ApiModelProperty(value = "货运身份:0=未申请;1=申请中;2=已通过;3=未通过;", example = "1") //弃用
    private Integer driverIdentity;
    @ApiModelProperty(value = "供餐身份:0=未申请;1=申请中;2=已通过;3=未通过;", example = "1") //弃用
    private Integer chefIdentity;
    @ApiModelProperty(value = "当前余额(单位:分)", example = "1")
    private Long amount;
@@ -163,18 +154,6 @@
    @ApiModelProperty(value = "头像全路径")
    @TableField(exist = false)
    private String fullCoverImage;
    @ApiModelProperty(value = "用工身份", example = "1")
    @TableField(exist = false)
    private IdentityInfo workerIdentityModel;
    @ApiModelProperty(value = "司机身份信息", example = "1")
    @TableField(exist = false)
    private IdentityInfo driverIdentityModel;
    @ApiModelProperty(value = "送餐身份信息", example = "1")
    @TableField(exist = false)
    private IdentityInfo chefIdentityModel;
    @ApiModelProperty(value = "接单权重", example = "1")
    @TableField(exist = false)
server/services/src/main/java/com/doumee/dao/business/model/MemberRevenue.java
@@ -32,7 +32,8 @@
    private Integer createUser;
    @ApiModelProperty(value = "创建时间")
    @ExcelColumn(name="交易时间",index = 8,dateFormat = "yyyy-MM-dd HH:mm:ss",width = 10)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @ExcelColumn(name = "收支时间", index = 3, dateFormat = "yyyy-MM-dd HH:mm:ss", width = 16)
    private Date createTime;
    @ApiModelProperty(value = "更新人编码", example = "1")
@@ -48,10 +49,11 @@
    private Integer memberId;
    @ApiModelProperty(value = "变动类型:0=用工单收入;1=货运单收入;2=供餐单收入;3=提现申请;4=提现失败退回", example = "1")
    @ExcelColumn(name = "业务类型", valueMapping = "0=用工单收入;1=货运单收入;2=供餐单收入;3=提现申请;4=提现失败退回;", index = 4, width = 12)
    private Integer type;
    @ApiModelProperty(value = "收支类型:1=收入;-1=支出;", example = "1")
    @ExcelColumn(name="收支类型", valueMapping="1=收入;-1=支出;",index = 5,width = 5)
    @ExcelColumn(name = "收入/支出", valueMapping = "1=收入;-1=支出;", index = 1, width = 8)
    private Integer optType;
    @ApiModelProperty(value = "变动金额", example = "1")
@@ -70,13 +72,15 @@
    private Integer objType;
    @ApiModelProperty(value = "业务状态:0=成功;1=失败;2=处理中;", example = "1")
    @ExcelColumn(name = "状态", valueMapping = "0=成功;1=失败;2=处理中;", index = 6, width = 8)
    private Integer status;
    @ApiModelProperty(value = "变动类型:0=用工单收入;1=货运单收入;2=供餐单收入;3=提现申请;", example = "1")
    @TableField(exist = false)
    @ExcelColumn(name="变动业务类型",index = 7,width = 8)
    private String typeName;
    @ApiModelProperty(value = "微信交易流水号", example = "1")
    @ExcelColumn(name="微信交易流水号",index = 9,width = 8)
    @ExcelColumn(name = "交易号", index = 5, width = 15)
    private String transactionNo;
    @ApiModelProperty(value = "变查询开始时间", example = "1")
@@ -87,23 +91,23 @@
    private Date endTime;
    @ApiModelProperty(value = "变动金额(元)", example = "1")
    @TableField(exist = false)
    @ExcelColumn(name="变动金额(元)",index = 6,width = 8)
    @ExcelColumn(name = "金额(元)", index = 2, width = 10)
    private Double amountInfo;
    @ApiModelProperty(value = "用户姓名", example = "1")
    @TableField(exist = false)
    @ExcelColumn(name="用户姓名",index = 3,width = 5)
    private String memberName;
    @ApiModelProperty(value = "用户昵称", example = "1")
    @TableField(exist = false)
    @ExcelColumn(name="用户昵称",index = 2,width = 5)
    private String memberNickname;
    @ApiModelProperty(value = "用户手机号", example = "1")
    @TableField(exist = false)
    @ExcelColumn(name="用户手机号",index = 4,width = 6)
    private String memberPhone;
    @ApiModelProperty(value = "用户openId", example = "1")
    @TableField(exist = false)
    @ExcelColumn(name="用户openid" ,index = 1,width = 6)
    private String openid;
    @ApiModelProperty(value = "用工类型:0=采摘工;1=分拣工;2=包装工", example = "1")
    @TableField(exist = false)
server/services/src/main/java/com/doumee/dao/business/model/Multifile.java
@@ -53,9 +53,9 @@
    @ApiModelProperty(value = "类型0图片 1视频 2其他", example = "1")
    private Integer type;
    @ApiModelProperty(value = "关联对象类型:0=门店其他材料;1=门店内部照片;2=订单寄存图片;3=订单取件图片;4=司机取件图片;5=司机完成图片;6=司机实名认证车辆照片;" +
    @ApiModelProperty(value = "关联对象类型:0=门店其他材料;1=门店内部照片;2=订单寄存图片;3=门店入库图片;4=司机取件图片;5=司机完成图片;6=司机实名认证车辆照片;" +
            "7=司机实名认证驾驶证照片;8=司机实名认证其他图片;" +
            "9=门店门头照;10=社保缴纳证明;11=有效劳动合同;", example = "1")
            "9=门店门头照;10=社保缴纳证明;11=有效劳动合同;12=下单图片;13=门店出库图片;", example = "1")
    private Integer objType;
    @ApiModelProperty(value = "文件地址")
server/services/src/main/java/com/doumee/dao/business/model/OrderLog.java
@@ -69,7 +69,8 @@
    @ApiModelProperty(value = "操作后数据")
    private String afterInfo;
    @ApiModelProperty(value = "订单状态:0=待接单;1=已接单;2=进行中;3=已完成;99=已取消;", example = "0")
    @ApiModelProperty(value = "订单状态:就地寄存状态:0=待支付;1=待寄存;2=已寄存;3=待取件;6=存在逾期;7=已完成;96:订单关闭(退款);97:取消逾期;98=取消中;99=已取消; " +
            " 异地寄存状态:0=待支付;1=待寄存;2=已寄存;3=已接单;4=派送中;5=已到店/已送达;6=存在逾期;7=已完成;96:订单关闭(退款);97:取消逾期;98=取消中;99=已取消;", example = "0")
    private Integer orderStatus;
    @ApiModelProperty(value = "操作人类型:0=用户;1=司机;2=门店;3=系统管理员", example = "0")
server/services/src/main/java/com/doumee/dao/business/model/Orders.java
@@ -66,7 +66,8 @@
    private Integer selfTake;
    @ApiModelProperty(value = "订单状态", example = "0")
    @ExcelColumn(name = "订单状态", valueMapping = "0=待支付;1=待寄存;2=已寄存;3=待取件;6=存在逾期;7=已完成;98=已挂起;99=已取消;")
    @ExcelColumn(name = "订单状态", valueMapping = "就地寄存状态:0=待支付;1=待寄存;2=已寄存;5=待取件;6=存在逾期;7=已完成;96:订单关闭(退款);97:取消逾期;98=取消中;99=已取消;" +
            "异地寄存状态:0=待支付;1=待寄存;2=已寄存;3=已接单;4=派送中;5=已到店/已送达;6=存在逾期;7=已完成;96:订单关闭(退款);97:取消逾期;98=取消中;99=已取消;")
    private Integer status;
    @ApiModelProperty(value = "是否异常订单:0=否;1=是", example = "0")
@@ -89,13 +90,13 @@
    @ExcelColumn(name = "存件店铺")
    private Integer depositShopId;
    @ApiModelProperty(value = "预计到店存件时间起")
    @ApiModelProperty(value = "预计到店存件时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date arriveStartTime;
    private Date expectedDepositTime;
    @ApiModelProperty(value = "预计到店存件时间止")
    @ApiModelProperty(value = "预计到店取件时间止")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date arriveEndTime;
    private Date expectedTakeTime;
    @ApiModelProperty(value = "存件地点")
    @ExcelColumn(name = "存件地点")
@@ -103,7 +104,7 @@
    @ApiModelProperty(value = "存件地点描述")
    @ExcelColumn(name = "存件地点描述")
    private String depositLocationReamrk;
    private String depositLocationRemark;
    @ApiModelProperty(value = "存件点经度")
    private BigDecimal depositLat;
@@ -121,7 +122,7 @@
    @ApiModelProperty(value = "取件地点详细地址")
    @ExcelColumn(name = "取件地点详细地址")
    private String takeLocationReamrk;
    private String takeLocationRemark;
    @ApiModelProperty(value = "取件经度")
    private BigDecimal takeLat;
@@ -237,6 +238,10 @@
    @ExcelColumn(name = "指派司机")
    private Integer assignDriverId;
    @ApiModelProperty(value = "基础寄存费用(分)")
    @ExcelColumn(name = "基础寄存费用")
    private Long basicAmount;
    @ApiModelProperty(value = "订单总费用(分)")
    @ExcelColumn(name = "订单总费用")
    private Long totalAmount;
@@ -256,6 +261,16 @@
    @ApiModelProperty(value = "逾期天数")
    @ExcelColumn(name = "逾期天数")
    private Integer overdueDays;
    @ApiModelProperty(value = "是否逾期: 0=否 1=是 2=已支付")
    @ExcelColumn(name = "是否逾期", valueMapping = "0=否;1=是;2=已支付;")
    private Integer overdueStatus;
    @ApiModelProperty(value = "会员订单核销码")
    private String memberVerifyCode;
    @ApiModelProperty(value = "司机订单核销码")
    private String driverVerifyCode;
    @ApiModelProperty(value = "支付状态:0=未支付;1=已支付", example = "0")
    @ExcelColumn(name = "支付状态", valueMapping = "0=未支付;1=已支付;")
@@ -333,6 +348,9 @@
    @ApiModelProperty(value = "结算状态:0=待结算;1=已结算;")
    private Integer settlementStatus;
    @ApiModelProperty(value = "三方订单号")
    private String outTradeNo;
    @TableField(exist = false)
    @ApiModelProperty(value = "创建开始时间(查询用)", example = "2026-01-01")
    @JsonFormat(pattern = "yyyy-MM-dd")
@@ -347,4 +365,32 @@
    @ApiModelProperty(value = "订单级别")
    private String orderLevel;
    @TableField(exist = false)
    @ApiModelProperty(value = "司机关键字(姓名/手机号)")
    private String driverKeyword;
    @TableField(exist = false)
    @ApiModelProperty(value = "寄件门店名称(关联查询)")
    private String depositShopName;
    @TableField(exist = false)
    @ApiModelProperty(value = "寄件门店联系人(关联查询)")
    private String depositShopLinkName;
    @TableField(exist = false)
    @ApiModelProperty(value = "寄件门店联系电话(关联查询)")
    private String depositShopLinkPhone;
    @TableField(exist = false)
    @ApiModelProperty(value = "寄件门店地址(关联查询)")
    private String depositShopAddress;
    @TableField(exist = false)
    @ApiModelProperty(value = "取件门店名称(关联查询)")
    private String takeShopName;
    @TableField(exist = false)
    @ApiModelProperty(value = "取件门店地址(关联查询)")
    private String takeShopAddress;
}
server/services/src/main/java/com/doumee/dao/business/model/OrdersDetail.java
@@ -60,11 +60,16 @@
    @ExcelColumn(name = "尺寸名称")
    private String luggageName;
    @ApiModelProperty(value = "尺寸详情")
    @ExcelColumn(name = "尺寸详情")
    private String luggageDetail;
    @ApiModelProperty(value = "数量", example = "1")
    @ExcelColumn(name = "数量")
    private Integer num;
    @ApiModelProperty(value = "就地存取 - 单价(分)")
    @ApiModelProperty(value = "单价(分)")
    @ExcelColumn(name = "单价")
    private Long unitPrice;
@@ -84,4 +89,8 @@
    @ExcelColumn(name = "超出首单里程单价")
    private Long extraPrice;
    @ApiModelProperty(value = "就地存取 - 单价(分)")
    @ExcelColumn(name = "就地存取 - 单价")
    private Long locallyPrice;
}
server/services/src/main/java/com/doumee/dao/business/model/PricingRule.java
@@ -52,7 +52,7 @@
    @ExcelColumn(name = "类型", index = 2, width = 10, valueMapping = "0=就地存取规则;1=异地存取规则;2=预计失效;3=门店注册押金;4=分成比例;")
    private Integer type;
    @ApiModelProperty(value = "参数1:type (0/1)=  关联 物品尺寸(category type =4);type (2)= 默认 1 ; type (3)  = 企业类型(0=企业;1=个人);type (4) = 类型(企业/个人/配送员)")
    @ApiModelProperty(value = "参数1:type (0/1)=  关联 物品尺寸(category type =4);type (2) (默认 1标速达;2=极速达) ; type (3)  = 企业类型(0=企业;1=个人);type (4) = 类型(企业/个人/配送员)")
    private String fieldA;
    @ApiModelProperty(value = "参数2:type (0)=  收费单价 ;type (1)= 配送起步里程公里数 ;type (2)= 起送里程 km ; type (3)  = 押金;type (4) = 地点类型(寄件点/取件点/分成比例)")
server/services/src/main/java/com/doumee/dao/business/model/Revenue.java
@@ -34,7 +34,7 @@
    @ApiModelProperty(value = "创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @ExcelColumn(name = "创建时间", index = 6, width = 16, dateFormat = "yyyy-MM-dd HH:mm:ss")
    @ExcelColumn(name = "收支时间", index = 3, width = 16, dateFormat = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    @ApiModelProperty(value = "更新人编码", example = "1")
@@ -54,15 +54,15 @@
    private Integer memberType;
    @ApiModelProperty(value = "是否生效:0=入账中;1=已入账;")
    @ExcelColumn(name = "是否生效", valueMapping = "0=入账中;1=已入账;", index = 5, width = 8)
    @ExcelColumn(name = "状态", valueMapping = "0=入账中;1=已入账;", index = 6, width = 8)
    private Integer vaildStatus;
    @ApiModelProperty(value = "类型:0=完成订单;1=提现支出;2=提现退回;3=平台奖励", example = "0")
    @ExcelColumn(name = "变动类型", valueMapping = "0=完成订单;1=提现支出;2=提现退回;3=平台奖励;", index = 3, width = 10)
    @ExcelColumn(name = "业务类型", valueMapping = "0=完成订单;1=提现支出;2=提现退回;3=平台奖励;", index = 4, width = 12)
    private Integer type;
    @ApiModelProperty(value = "收支类型:1=收入;-1=支出", example = "1")
    @ExcelColumn(name = "收支类型", valueMapping = "1=收入;-1=支出;", index = 1, width = 8)
    @ExcelColumn(name = "收入/支出", valueMapping = "1=收入;-1=支出;", index = 1, width = 8)
    private Integer optType;
    @ApiModelProperty(value = "金额(分)")
@@ -84,7 +84,7 @@
    private Integer status;
    @ApiModelProperty(value = "订单号")
    @ExcelColumn(name = "订单号", index = 4, width = 16)
    @ExcelColumn(name = "交易号", index = 5, width = 16)
    private String orderNo;
    @ApiModelProperty(value = "查询开始时间")
@@ -97,7 +97,7 @@
    @ApiModelProperty(value = "变动金额(元)")
    @TableField(exist = false)
    @ExcelColumn(name = "变动金额(元)", index = 2, width = 10)
    @ExcelColumn(name = "金额(元)", index = 2, width = 10)
    private Double amountInfo;
}
server/services/src/main/java/com/doumee/dao/business/model/ShopInfo.java
@@ -125,7 +125,7 @@
    @ApiModelProperty(value = "审批备注")
    private String auditRemark;
    @ApiModelProperty(value = "OPENID")
    @ApiModelProperty(value = "当前登录用户openid")
    private String openid;
    @ApiModelProperty(value = "支付状态:0=待支付;1=已支付", example = "0")
@@ -155,6 +155,29 @@
    @ApiModelProperty(value = "配送范围(km)")
    private BigDecimal deliveryArea;
    @ApiModelProperty(value = "支付宝提现账户")
    private String aliAccount;
    @ApiModelProperty(value = "营业时间")
    private String shopHours;
    @ApiModelProperty(value = "门店介绍")
    private String content;
    @ApiModelProperty(value = "门店头像")
    private String coverImg;
    @ApiModelProperty(value = "寄存类型(逗号分隔的category主键)")
    private String depositTypes;
    @ApiModelProperty(value = "收费标准")
    private String feeStandard;
    @ApiModelProperty(value = "门店营业类型:0=非全天;1=全天", example = "0")
    private Integer businessType;
    // 非持久化:附件列表
    @TableField(exist = false)
    @ApiModelProperty(value = "门店门头照", hidden = true)
@@ -186,4 +209,8 @@
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date createEndTime;
    @TableField(exist = false)
    @ApiModelProperty(value = "审批状态列表(查询用,逗号分隔)", example = "0,1,2")
    private List<Integer> auditStatusList;
}
server/services/src/main/java/com/doumee/dao/business/model/Smsrecord.java
@@ -46,6 +46,9 @@
    private String remark;
    @ApiModelProperty(value = "手机号")
    private Integer memberId;
    @ApiModelProperty(value = "手机号")
    @ExcelColumn(name = "手机号", index = 2, width = 12)
    private String phone;
server/services/src/main/java/com/doumee/dao/business/model/WithdrawalOrders.java
@@ -56,6 +56,11 @@
    @ExcelColumn(name = "会员主键")
    private Integer memberId;
    @ApiModelProperty(value = "用户类型:1= 司机;2=店铺;", example = "1")
    @ExcelColumn(name = "用户类型:1= 司机;2=店铺;")
    private Integer memberType;
    @ApiModelProperty(value = "提现金额(单位:分)")
    @ExcelColumn(name = "提现金额")
    private Long amount;
@@ -88,6 +93,19 @@
    @ExcelColumn(name = "提现申请单号")
    private String outBillNo;
    @ApiModelProperty(value = "支付宝提现账户")
    private String aliAccount;
    @ApiModelProperty(value = "审批操作人(关联system_user)", example = "1")
    private Integer userId;
    @ApiModelProperty(value = "审批时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date approveTime;
    @ApiModelProperty(value = "审批备注")
    private String approveRemark;
    @TableField(exist = false)
    @ApiModelProperty(value = "创建开始时间(查询用)", example = "2026-01-01")
    @JsonFormat(pattern = "yyyy-MM-dd")
@@ -98,4 +116,29 @@
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date createEndTime;
    @TableField(exist = false)
    @ApiModelProperty(value = "审批人名称")
    private String updateUserName;
    @TableField(exist = false)
    @ApiModelProperty(value = "司机姓名")
    private String memberName;
    @TableField(exist = false)
    @ApiModelProperty(value = "司机联系电话")
    private String memberTelephone;
    @TableField(exist = false)
    @ApiModelProperty(value = "门店名称")
    private String shopName;
    @TableField(exist = false)
    @ApiModelProperty(value = "联系人")
    private String linkName;
    @TableField(exist = false)
    @ApiModelProperty(value = "门店详情")
    private ShopInfo shopInfo;
}
server/services/src/main/java/com/doumee/dao/dto/AuditDTO.java
@@ -23,6 +23,9 @@
    @ApiModelProperty(value = "审批描述")
    private String auditRemark;
    @ApiModelProperty(value = "司机定级:5=S;4=A;3=B;2=C;1=D(审批通过时必填)", example = "5")
    private Integer driverLevel;
    @ApiModelProperty(value = "审批人编码(服务端填充)", hidden = true)
    private Integer auditUser;
server/services/src/main/java/com/doumee/dao/dto/EstimatedDeliverySaveDTO.java
@@ -6,6 +6,7 @@
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.List;
/**
 * 预计时效配置保存请求
@@ -20,19 +21,32 @@
    @NotNull(message = "城市主键不能为空")
    private Integer cityId;
    @ApiModelProperty(value = "起送里程(公里)", required = true, example = "10")
    @NotNull(message = "起送里程不能为空")
    private String startDistance;
    @ApiModelProperty(value = "时效配置列表(fieldA=1标速达,fieldA=2极速达)")
    @NotNull(message = "时效配置列表不能为空")
    private List<EstimatedDeliveryItem> items;
    @ApiModelProperty(value = "起送时长", required = true, example = "2")
    @NotNull(message = "起送时长不能为空")
    private String startTime;
    @Data
    @ApiModel("预计时效配置项")
    public static class EstimatedDeliveryItem implements Serializable {
    @ApiModelProperty(value = "续送里程(公里)", required = true, example = "5")
    @NotNull(message = "续送里程不能为空")
    private String continueDistance;
        @ApiModelProperty(value = "类型:1=标速达;2=极速达", required = true, example = "1")
        @NotNull(message = "类型不能为空")
        private Integer fieldA;
    @ApiModelProperty(value = "续送时长", required = true, example = "1")
    @NotNull(message = "续送时长不能为空")
    private String continueTime;
        @ApiModelProperty(value = "起送里程(公里)", required = true, example = "10")
        @NotNull(message = "起送里程不能为空")
        private String startDistance;
        @ApiModelProperty(value = "起送时长", required = true, example = "2")
        @NotNull(message = "起送时长不能为空")
        private String startTime;
        @ApiModelProperty(value = "续送里程(公里)", required = true, example = "5")
        @NotNull(message = "续送里程不能为空")
        private String continueDistance;
        @ApiModelProperty(value = "续送时长", required = true, example = "1")
        @NotNull(message = "续送时长不能为空")
        private String continueTime;
    }
}
server/services/src/main/java/com/doumee/dao/dto/OperationConfigDTO.java
@@ -16,7 +16,7 @@
    @ApiModelProperty(value = "司机每日取消次数", required = true)
    private String driverDailyCancelLimit;
    @ApiModelProperty(value = "未取件折扣", required = true)
    @ApiModelProperty(value = "未取件逾期折扣", required = true)
    private String unpickedDiscount;
    @ApiModelProperty(value = "订单结算日期", required = true)
server/services/src/main/java/com/doumee/dao/dto/ShopLoginDTO.java
@@ -1,21 +1,18 @@
package com.doumee.dao.web.dto;
package com.doumee.dao.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("经销商登录请求对象")
@ApiModel("门店登录请求对象")
public class ShopLoginDTO {
//    @ApiModelProperty(value = "关联用户主键")
//    private Integer memberId;
    @ApiModelProperty(value = "openid")
    private String openid;
    @ApiModelProperty(value = "用户名称")
    private String userName;
    @ApiModelProperty(value = "登录手机号")
    private String telephone;
    @ApiModelProperty(value = "密码")
    private String password;
server/services/src/main/java/com/doumee/dao/dto/UpdMobileRequest.java
@@ -1,11 +1,8 @@
package com.doumee.dao.web.request;
package com.doumee.dao.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
 * Created by IntelliJ IDEA.
server/services/src/main/java/com/doumee/dao/vo/AccountResponse.java
@@ -27,4 +27,16 @@
    @ApiModelProperty(value = "用户信息")
    private Member member;
    @ApiModelProperty(value = "我注册的门店主键(门店用户时返回)")
    private Integer shopId;
    @ApiModelProperty(value = "我注册的门店审核状态(门店用户时返回): 0=待审核 1=已通过 2=已驳回 3=已缴纳保证金")
    private Integer shopAuditStatus;
    @ApiModelProperty(value = "门店token")
    private String loginShopToken;
}
server/services/src/main/java/com/doumee/dao/vo/EstimatedDeliveryVO.java
@@ -21,6 +21,9 @@
    @ApiModelProperty(value = "城市主键")
    private Integer cityId;
    @ApiModelProperty(value = "类型:1=标速达;2=极速达")
    private Integer fieldA;
    @ApiModelProperty(value = "起送里程(公里)")
    private String startDistance;
server/services/src/main/java/com/doumee/dao/vo/ItemPriceVO.java
@@ -22,6 +22,9 @@
    @ApiModelProperty("物品类型名称")
    private String categoryName;
    @ApiModelProperty("物品类型详情(寄存说明)")
    private String detail;
    @ApiModelProperty("数量")
    private Integer quantity;
@@ -44,4 +47,7 @@
    @ApiModelProperty("超出距离单价(分)")
    private Long extraPrice;
    @ApiModelProperty("就地存取单价(分/天)")
    private Long locallyPrice;
}
server/services/src/main/java/com/doumee/dao/vo/MemberDetailVO.java
@@ -26,6 +26,9 @@
    @ApiModelProperty(value = "会员姓名")
    private String name;
    @ApiModelProperty(value = "openid")
    private String openid;
    @ApiModelProperty(value = "授权手机号")
    private String telephone;
server/services/src/main/java/com/doumee/dao/vo/OrderDetailVO.java
@@ -1,6 +1,7 @@
package com.doumee.dao.vo;
import com.doumee.dao.business.model.Orders;
import com.doumee.dao.business.model.OrdersRefund;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -59,25 +60,17 @@
    private List<String> storeOutImages;
    @ApiModelProperty(value = "物品明细列表")
    private List<OrderDetailItemVO> detailList;
    private List<OrderItemVO> detailList;
    /**
     * 物品明细项
     */
    @Data
    @ApiModel("物品明细项")
    public static class OrderDetailItemVO {
    @ApiModelProperty(value = "退款记录(订单状态为取消/退款时返回)")
    private OrdersRefund ordersRefund;
        @ApiModelProperty(value = "尺寸名称")
        private String luggageName;
    @ApiModelProperty(value = "退款取件图片(已存件申请取消时返回)")
    private List<String> refundTakeImages;
        @ApiModelProperty(value = "数量")
        private Integer num;
    @ApiModelProperty(value = "平台操作人名称(平台直接取消时返回)")
    private String platformUserName;
        @ApiModelProperty(value = "单价(元)")
        private Double unitPriceYuan;
        @ApiModelProperty(value = "小计费用(元)")
        private Double subtotal;
    }
    @ApiModelProperty(value = "接单司机姓名")
    private String driverName;
}
server/services/src/main/java/com/doumee/dao/vo/OrderDispatchVO.java
@@ -28,25 +28,5 @@
    private String typeDesc;
    @ApiModelProperty(value = "订单物品详情")
    private List<DispatchItemVO> detailList;
    /**
     * 物品明细项
     */
    @Data
    @ApiModel("派单物品明细项")
    public static class DispatchItemVO {
        @ApiModelProperty(value = "尺寸名称")
        private String luggageName;
        @ApiModelProperty(value = "数量")
        private Integer num;
        @ApiModelProperty(value = "单价(元)")
        private Double unitPriceYuan;
        @ApiModelProperty(value = "小计费用(元)")
        private Double subtotal;
    }
    private List<OrderItemVO> detailList;
}
server/services/src/main/java/com/doumee/dao/vo/OrderItemVO.java
@@ -13,9 +13,15 @@
@ApiModel("订单物品明细项")
public class OrderItemVO {
    @ApiModelProperty(value = "分类名称")
    private String typeName;
    @ApiModelProperty(value = "尺寸名称")
    private String luggageName;
    @ApiModelProperty(value = "尺寸详情")
    private String luggageDetail;
    @ApiModelProperty(value = "数量")
    private Integer num;
server/services/src/main/java/com/doumee/dao/vo/PayResponse.java
@@ -1,4 +1,4 @@
package com.doumee.dao.web.response.goods;
package com.doumee.dao.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@@ -18,9 +18,6 @@
    @ApiModelProperty(value = "支付订单主键")
    private Integer orderId;
    @ApiModelProperty(value = "支付类型:0=现金支付;1=纯积分支付")
    private Integer payType;
    @ApiModelProperty(value = "微信调起业务")
    private Object response;
server/services/src/main/java/com/doumee/dao/vo/ShopDetailVO.java
@@ -78,11 +78,20 @@
    @ApiModelProperty(value = "身份证正面照")
    private String idcardImg;
    @ApiModelProperty(value = "身份证正面照(全路径)")
    private String idcardImgUrl;
    @ApiModelProperty(value = "身份证反面照")
    private String idcardImgBack;
    @ApiModelProperty(value = "身份证反面照(全路径)")
    private String idcardImgBackUrl;
    @ApiModelProperty(value = "营业执照")
    private String businessImg;
    @ApiModelProperty(value = "营业执照(全路径)")
    private String businessImgUrl;
    @ApiModelProperty(value = "审批状态:0=待审批;1=审批通过;2=审批未通过;3=已支付押金")
    private Integer auditStatus;
@@ -95,6 +104,10 @@
    @ApiModelProperty(value = "审批备注")
    private String auditRemark;
    @ApiModelProperty(value = "审批人名称")
    private String auditName;
    @ApiModelProperty(value = "OPENID")
    private String openid;
@@ -110,7 +123,7 @@
    @ApiModelProperty(value = "配送范围(km)")
    private BigDecimal deliveryArea;
    // 附件列表
    // 附件列表(半路径)
    @ApiModelProperty(value = "门店门头照")
    private List<String> storeFrontImgs;
@@ -126,6 +139,22 @@
    @ApiModelProperty(value = "社保缴纳证明")
    private List<String> socialSecurityImgs;
    // 附件列表(全路径)
    @ApiModelProperty(value = "门店门头照(全路径)")
    private List<String> storeFrontImgUrls;
    @ApiModelProperty(value = "门店内部照(全路径)")
    private List<String> storeInteriorImgUrls;
    @ApiModelProperty(value = "其他材料(全路径)")
    private List<String> otherMaterialImgUrls;
    @ApiModelProperty(value = "有效劳动合同(全路径)")
    private List<String> laborContractImgUrls;
    @ApiModelProperty(value = "社保缴纳证明(全路径)")
    private List<String> socialSecurityImgUrls;
    @ApiModelProperty(value = "图片前缀地址")
    private String imgPrefix;
server/services/src/main/java/com/doumee/dao/vo/UserCenterVO.java
@@ -37,9 +37,6 @@
    @ApiModelProperty(value = "服务介绍")
    private String serverIntroduce;
    @ApiModelProperty(value = "收费标准")
    private String feeStandards;
    @ApiModelProperty(value = "关于我们")
    private String aboutUs;
server/services/src/main/java/com/doumee/service/business/AddrService.java
@@ -46,4 +46,14 @@
     * 根据ID查询地址(含省市区信息)
     */
    Addr findByIdWithArea(Integer id);
    /**
     * 小程序端新增地址
     */
    Integer createByMember(Addr addr, Integer memberId);
    /**
     * 小程序端修改地址
     */
    void updateByMember(Addr addr, Integer memberId);
}
server/services/src/main/java/com/doumee/service/business/AreasService.java
@@ -133,4 +133,11 @@
    List<Areas> getCityList(Areas areas);
    /**
     * 查询已开放的城市列表(status=1,type=1),附带首字母并按首字母排序
     *
     * @return 城市列表
     */
    List<Areas> getOpenCityList();
}
server/services/src/main/java/com/doumee/service/business/BannerService.java
@@ -33,5 +33,13 @@
    PageData<Banner> findPage(PageWrap<Banner> pageWrap);
    void updateStatus(Banner banner);
    long count(Banner banner);
    /**
     * 根据位置获取轮播图列表(含图片全路径)
     * @param position 位置
     */
    List<Banner> findListByPosition(Integer position);
}
server/services/src/main/java/com/doumee/service/business/CategoryService.java
@@ -97,4 +97,12 @@
    List<Category> getCategoryList(Integer type);
    /**
     * 根据城市主键查询已开通的物品尺寸(category type=4)
     * 通过 pricing_rule type=1 city_id 关联 category.id = pricing_rule.fieldA
     * @param cityId 城市主键
     * @return 物品尺寸列表
     */
    List<Category> getCitySizeList(Integer cityId);
}
server/services/src/main/java/com/doumee/service/business/DriverInfoService.java
@@ -146,4 +146,19 @@
     */
    void auditVerify(com.doumee.dao.dto.AuditDTO auditDTO);
    /**
     * 管理端查询司机详情(含附件图片、车辆类型名称)
     *
     * @param id 司机主键
     * @return DriverInfo
     */
    DriverInfo getDetail(Integer id);
    /**
     * 修改司机状态(启用/禁用)
     *
     * @param dto 修改状态请求
     */
    void changeStatus(com.doumee.dao.dto.ChangeStatusDTO dto);
}
server/services/src/main/java/com/doumee/service/business/MemberService.java
@@ -10,8 +10,12 @@
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.Member;
import com.doumee.dao.dto.MemberListQueryDTO;
import com.doumee.dao.dto.UpdMobileRequest;
import com.doumee.dao.dto.WxPhoneRequest;
import com.doumee.dao.vo.AccountResponse;
import com.doumee.dao.vo.MemberDetailVO;
import com.doumee.dao.vo.MemberListVO;
import com.doumee.dao.vo.UserCenterVO;
import me.chanjar.weixin.common.error.WxErrorException;
import org.apache.commons.lang3.StringUtils;
@@ -135,12 +139,6 @@
     */
    void editMemberInfo(Member member);
//    /**
//     * 切换用工身份
//     * @param member
//     */
//    void editUseIdentity(Member member);
    /**
     * 个人信息
     * @param memberId
@@ -153,4 +151,36 @@
    void logOut(String token,Integer memberId);
    void logOff(String token,Integer memberId);
    /**
     * 验证手机号
     * @param request
     */
    void verifyUserPhone(UpdMobileRequest request);
    /**
     * 换绑手机号
     * @param request
     */
    void updateUserPhone(UpdMobileRequest request);
    /***************管理端会员列表接口*************/
    /**
     * 会员列表分页查询
     *
     * @param pageWrap 分页对象(model为MemberListQueryDTO)
     * @return PageData<MemberListVO>
     */
    PageData<MemberListVO> findMemberListPage(PageWrap<MemberListQueryDTO> pageWrap);
    /**
     * 查询会员详情
     *
     * @param id 会员ID
     * @return MemberDetailVO
     */
    MemberDetailVO findMemberDetail(Integer id);
}
server/services/src/main/java/com/doumee/service/business/OrdersService.java
@@ -3,7 +3,21 @@
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.Orders;
import com.doumee.dao.dto.CalculateLocalPriceDTO;
import com.doumee.dao.dto.CalculateRemotePriceDTO;
import com.doumee.dao.dto.CreateOrderDTO;
import com.doumee.dao.dto.DispatchDTO;
import com.doumee.dao.dto.MyOrderDTO;
import com.doumee.dao.vo.MyOrderDetailVO;
import com.doumee.dao.vo.MyOrderVO;
import com.doumee.dao.vo.OrderDetailVO;
import com.doumee.dao.vo.OrderDispatchVO;
import com.doumee.dao.vo.OrderSummaryVO;
import com.doumee.dao.vo.OverdueFeeVO;
import com.doumee.dao.vo.PayResponse;
import com.doumee.dao.vo.PriceCalculateVO;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -96,4 +110,154 @@
     */
    long count(Orders orders);
    /**
     * 查询订单详情
     *
     * @param id 主键
     * @return OrderDetailVO
     */
    OrderDetailVO findDetail(Integer id);
    /**
     * 会员端订单详情
     *
     * @param id       订单主键
     * @param memberId 会员主键
     * @return MyOrderDetailVO
     */
    MyOrderDetailVO findMyOrderDetail(Integer id, Integer memberId);
    /**
     * 会员取消订单(仅异地寄存)
     *
     * @param orderId   订单主键
     * @param memberId  会员主键
     * @param reason    取消原因
     */
    void cancelOrder(Integer orderId, Integer memberId, String reason);
    /**
     * 寄存订单支付回调处理
     *
     * @param outTradeNo 商户订单号
     * @param wxTradeNo  微信订单号
     */
    void handleStorageOrderPayNotify(String outTradeNo, String wxTradeNo);
    /**
     * 门店核销收件
     *
     * @param verifyCode 核销码
     * @param shopId     门店主键
     * @param images     图片地址列表(最多3张)
     * @param remark     备注
     */
    void shopVerifyOrder(String verifyCode, Integer shopId, List<String> images, String remark);
    /**
     * 门店端查询订单详情
     * 支持按订单主键或核销码查询,复用会员端详情逻辑
     *
     * @param orderId    订单主键(与verifyCode二选一)
     * @param verifyCode 核销码(与orderId二选一)
     * @return 订单详情
     */
    MyOrderDetailVO findShopOrderDetail(Integer orderId, String verifyCode);
    /**
     * 查询手动派单信息
     *
     * @param id 订单主键
     * @return OrderDispatchVO
     */
    OrderDispatchVO findDispatchInfo(Integer id);
    /**
     * 订单派单
     *
     * @param dto 派单参数
     */
    void dispatch(DispatchDTO dto);
    /**
     * 根据分页查询条件统计订单汇总
     *
     * @param pageWrap 分页查询条件
     * @return OrderSummaryVO
     */
    OrderSummaryVO findSummary(PageWrap<Orders> pageWrap);
    /**
     * 计算保价费用
     * @param declaredValue 报价金额
     * @return 保价费用
     */
    BigDecimal calculateInsuranceFee(BigDecimal declaredValue);
    /**
     * 计算就地存取预估费用
     * @param dto 就地存取计价请求参数
     * @return 价格计算结果
     */
    PriceCalculateVO calculateLocalPrice(CalculateLocalPriceDTO dto);
    /**
     * 计算异地存取预估费用
     * @param dto 异地存取计价请求参数
     * @return 价格计算结果
     */
    PriceCalculateVO calculateRemotePrice(CalculateRemotePriceDTO dto);
    /**
     * 创建订单并唤起微信支付
     * @param dto 创建订单请求参数
     * @param memberId 当前登录会员ID
     * @return 支付响应
     */
    PayResponse createOrder(CreateOrderDTO dto, Integer memberId);
    /**
     * 继续支付(待支付订单重新唤起微信支付)
     * @param orderId 订单主键
     * @param memberId 当前登录会员ID
     * @return 支付响应
     */
    PayResponse continuePay(Integer orderId, Integer memberId);
    /**
     * 小程序端-查询我的订单分页
     * @param pageWrap 分页查询参数(model含status)
     * @param memberId 会员主键
     * @return 分页结果
     */
    PageData<MyOrderVO> findMyOrderPage(PageWrap<MyOrderDTO> pageWrap, Integer memberId);
    /**
     * 查询订单超时费用
     * @param orderId 订单主键
     * @return 超时费用计算结果
     */
    OverdueFeeVO calculateOverdueFee(Integer orderId);
    /**
     * 司机核销(异地寄存且有取件门店)
     * 派送中(4) → 已到店(5),校验司机核销码
     *
     * @param verifyCode 司机核销码
     * @param images     图片地址列表(最多3张)
     * @param remark     备注
     * @param driverId   司机(会员)主键
     */
    void driverVerifyOrder(String verifyCode, List<String> images, String remark, Integer driverId);
    /**
     * 确认顾客已到店
     * 就地寄存/异地寄存,且存在取件门店,状态为待取件(5)
     * 检查逾期状态:逾期则标记逾期,未逾期则完成订单
     *
     * @param orderId 订单主键
     * @param shopId  当前操作门店主键
     */
    void confirmCustomerArrived(Integer orderId, Integer shopId);
}
server/services/src/main/java/com/doumee/service/business/PricingRuleService.java
@@ -14,6 +14,7 @@
import com.doumee.dao.vo.StoreDepositVO;
import com.doumee.dao.vo.RevenueShareVO;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -133,17 +134,26 @@
    List<RemoteDeliveryPricingVO> listRemoteDeliveryPricing(Integer cityId);
    /**
     * 保存预计时效配置(有则更新,无则新增)
     * 保存预计时效配置(标速达fieldA=1 + 极速达fieldA=2,有则更新无则新增)
     * @param request 保存请求
     */
    void saveEstimatedDelivery(EstimatedDeliverySaveDTO request);
    /**
     * 查询预计时效配置(有且只有一条,无数据返回空对象)
     * 查询预计时效配置列表(标速达fieldA=1 + 极速达fieldA=2)
     * @param cityId 城市主键
     * @return 预计时效配置
     * @return 预计时效配置列表
     */
    EstimatedDeliveryVO getEstimatedDelivery(Integer cityId);
    List<EstimatedDeliveryVO> getEstimatedDelivery(Integer cityId);
    /**
     * 根据城市、类型、距离计算预计时效时长
     * @param cityId 城市主键
     * @param fieldA 类型:1=标速达;2=极速达
     * @param distance 距离(公里)
     * @return 预计时长(小时),未找到配置返回null
     */
    BigDecimal calculateEstimatedTime(Integer cityId, Integer fieldA, BigDecimal distance);
    /**
     * 批量保存门店注册押金
server/services/src/main/java/com/doumee/service/business/ShopInfoService.java
@@ -4,13 +4,13 @@
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.ShopInfo;
import com.doumee.dao.business.model.Member;
import com.doumee.dao.dto.AuditDTO;
import com.doumee.dao.dto.ChangeStatusDTO;
import com.doumee.dao.dto.ResetPasswordDTO;
import com.doumee.dao.dto.ShopApplyDTO;
import com.doumee.dao.dto.ShopUpdateDTO;
import com.doumee.dao.dto.*;
import com.doumee.dao.vo.ShopLoginVO;
import com.doumee.dao.vo.ShopDetailVO;
import com.doumee.dao.vo.ShopNearbyVO;
import com.doumee.dao.vo.ShopWebDetailVO;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -148,4 +148,47 @@
     */
    void updateShop(ShopUpdateDTO dto);
    /**
     * 根据经纬度查询附近门店分页列表(启用+已交付押金,按距离升序)
     * @param dto 查询请求
     * @return 附近门店分页列表
     */
    PageData<ShopNearbyVO> findNearbyShops(PageWrap<ShopNearbyDTO> pageWrap);
    /**
     * 查询门店详情(小程序端,含照片集合和可选距离)
     * @param dto 查询请求
     * @return 门店详情
     */
    ShopWebDetailVO getShopWebDetail(ShopDetailQueryDTO dto);
    /**
     * 维护门店信息(支付押金后可操作)
     * @param memberId 会员主键
     * @param dto 维护请求
     */
    void maintainShopInfo(Integer memberId, ShopInfoMaintainDTO dto);
    /**
     * 查询门店维护信息(小程序端)
     * @param memberId 会员主键
     * @return 门店信息,无则返回null
     */
    ShopInfoMaintainDTO getShopMaintainInfo(Integer memberId);
    /**
     * 门店密码登录
     * @param dto 登录请求
     * @return 登录结果
     */
    ShopLoginVO shopPasswordLogin(ShopLoginDTO dto);
    /**
     * 门店静默登录(根据openid)
     * @param openid
     * @return 登录结果
     */
    ShopLoginVO shopSilentLogin(String openid);
}
server/services/src/main/java/com/doumee/service/business/SmsrecordService.java
@@ -96,4 +96,10 @@
     */
    long count(Smsrecord smsrecord);
    void sendSms(Integer memberId,String phone);
    void verifyCode(Integer memberId,String phone,String code);
}
server/services/src/main/java/com/doumee/service/business/WithdrawalOrdersService.java
@@ -3,6 +3,7 @@
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.WithdrawalOrders;
import com.doumee.dao.dto.WithdrawalApproveDTO;
import java.util.List;
@@ -96,4 +97,19 @@
     */
    long count(WithdrawalOrders withdrawalOrders);
    /**
     * 已提现统计金额(status=0/1),使用与分页查询相同的条件
     *
     * @param pageWrap 分页查询条件
     * @return 已提现总金额(单位:分)
     */
    Long totalAmount(PageWrap<WithdrawalOrders> pageWrap);
    /**
     * 提现审批
     *
     * @param dto 审批参数
     */
    void approve(WithdrawalApproveDTO dto);
}
server/services/src/main/java/com/doumee/service/business/impl/AddrServiceImpl.java
@@ -14,6 +14,7 @@
import com.doumee.dao.business.AddrMapper;
import com.doumee.dao.business.model.Addr;
import com.doumee.service.business.AddrService;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -168,4 +169,119 @@
        QueryWrapper<Addr> wrapper = new QueryWrapper<>(addr);
        return addrMapper.selectCount(wrapper);
    }
    @Override
    public List<Addr> findListWithArea(Integer memberId) {
        MPJLambdaWrapper<Addr> wrapper = new MPJLambdaWrapper<Addr>()
                .selectAll(Addr.class)
                .select("a3.name", Addr::getDistrictName)
                .select("a3.code", Addr::getDistrictCode)
                .select("a2.id", Addr::getCityId)
                .select("a2.name", Addr::getCityName)
                .select("a2.code", Addr::getCityCode)
                .select("a1.id", Addr::getProvinceId)
                .select("a1.name", Addr::getProvinceName)
                .select("a1.code", Addr::getProvinceCode)
                .leftJoin("areas a3 on a3.id = t.AREA_ID")
                .leftJoin("areas a2 on a2.id = a3.PARENT_ID")
                .leftJoin("areas a1 on a1.id = a2.PARENT_ID")
                .eq(Addr::getDeleted, Constants.ZERO)
                .eq(Addr::getMemberId, memberId)
                .orderByDesc(Addr::getIsDefault)
                .orderByDesc(Addr::getCreateTime);
        return addrMapper.selectJoinList(Addr.class, wrapper);
    }
    @Override
    public Addr findByIdWithArea(Integer id) {
        MPJLambdaWrapper<Addr> wrapper = new MPJLambdaWrapper<Addr>()
                .selectAll(Addr.class)
                .select("a3.name", Addr::getDistrictName)
                .select("a3.code", Addr::getDistrictCode)
                .select("a2.id", Addr::getCityId)
                .select("a2.name", Addr::getCityName)
                .select("a2.code", Addr::getCityCode)
                .select("a1.id", Addr::getProvinceId)
                .select("a1.name", Addr::getProvinceName)
                .select("a1.code", Addr::getProvinceCode)
                .leftJoin("areas a3 on a3.id = t.AREA_ID")
                .leftJoin("areas a2 on a2.id = a3.PARENT_ID")
                .leftJoin("areas a1 on a1.id = a2.PARENT_ID")
                .eq(Addr::getId, id);
        Addr addr = addrMapper.selectJoinOne(Addr.class, wrapper);
        if (Objects.isNull(addr)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        return addr;
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public Integer createByMember(Addr addr, Integer memberId) {
        validateAddr(addr);
        // 设为默认时,先清除该用户原有的默认地址
        if (Constants.equalsInteger(addr.getIsDefault(), Constants.ONE)) {
            clearDefault(memberId);
        }
        addr.setMemberId(memberId);
        addr.setDeleted(Constants.ZERO);
        addr.setCreateTime(new Date());
        addr.setCreateUser(memberId);
        addr.setUpdateTime(new Date());
        addr.setUpdateUser(memberId);
        addrMapper.insert(addr);
        return addr.getId();
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void updateByMember(Addr addr, Integer memberId) {
        if (Objects.isNull(addr) || Objects.isNull(addr.getId())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        validateAddr(addr);
        // 设为默认时,先清除该用户原有的默认地址
        if (Constants.equalsInteger(addr.getIsDefault(), Constants.ONE)) {
            clearDefault(memberId);
        }
        addr.setMemberId(memberId);
        addr.setUpdateTime(new Date());
        addr.setUpdateUser(memberId);
        addrMapper.updateById(addr);
    }
    private void validateAddr(Addr addr) {
        if (Objects.isNull(addr.getAreaId())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "区域不能为空");
        }
        if (StringUtils.isBlank(addr.getAddr())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "详细地址不能为空");
        }
        if (Objects.isNull(addr.getIsDefault())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "是否默认不能为空");
        }
        if (StringUtils.isBlank(addr.getName())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "姓名不能为空");
        }
        if (StringUtils.isBlank(addr.getPhone())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "电话不能为空");
        }
        if (Objects.isNull(addr.getLongitude())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "经度不能为空");
        }
        if (Objects.isNull(addr.getLatitude())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "纬度不能为空");
        }
    }
    /**
     * 清除该用户所有地址的默认标记
     */
    private void clearDefault(Integer memberId) {
        addrMapper.update(new UpdateWrapper<Addr>().lambda()
                .set(Addr::getIsDefault, Constants.ZERO)
                .eq(Addr::getMemberId, memberId)
                .eq(Addr::getDeleted, Constants.ZERO)
                .eq(Addr::getIsDefault, Constants.ONE));
    }
}
server/services/src/main/java/com/doumee/service/business/impl/AreasServiceImpl.java
@@ -518,4 +518,23 @@
        return dataList;
    }
    @Override
    public List<Areas> getOpenCityList() {
        QueryWrapper<Areas> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(Areas::getType, Constants.ONE)
                .eq(Areas::getStatus, Constants.ONE)
                .eq(Areas::getIsdeleted, Constants.ZERO)
                .orderByAsc(Areas::getSortnum);
        List<Areas> list = areasMapper.selectList(qw);
        if (list != null) {
            for (Areas c : list) {
                c.setFullspell(PinYinUtil.getFullSpell(c.getName()));
                c.setFirstSpell(PinYinUtil.getFirstFirstSpell(c.getName()));
            }
            Collections.sort(list);
        }
        return list;
    }
}
server/services/src/main/java/com/doumee/service/business/impl/BannerServiceImpl.java
@@ -4,19 +4,25 @@
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.doumee.biz.system.SystemDictDataBiz;
import com.doumee.core.constants.Constants;
import com.doumee.core.constants.ResponseStatus;
import com.doumee.core.exception.BusinessException;
import com.doumee.core.model.LoginUserInfo;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.core.utils.Utils;
import com.doumee.dao.business.BannerMapper;
import com.doumee.dao.business.model.Banner;
import com.doumee.service.business.BannerService;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@@ -31,15 +37,28 @@
    @Autowired
    private BannerMapper bannerMapper;
    @Autowired
    private SystemDictDataBiz systemDictDataBiz;
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public Integer create(Banner banner) {
        validateBanner(banner, false);
        LoginUserInfo loginUserInfo = (LoginUserInfo) SecurityUtils.getSubject().getPrincipal();
        banner.setDeleted(Constants.ZERO);
        banner.setCreateTime(new Date());
        banner.setCreateUser(loginUserInfo.getId());
        banner.setUpdateTime(new Date());
        banner.setUpdateUser(loginUserInfo.getId());
        bannerMapper.insert(banner);
        return banner.getId();
    }
    @Override
    public void deleteById(Integer id) {
        bannerMapper.deleteById(id);
        bannerMapper.update(new UpdateWrapper<Banner>().lambda()
                .set(Banner::getDeleted, Constants.ONE)
                .eq(Banner::getId, id));
    }
    @Override
@@ -53,11 +72,29 @@
        if (CollectionUtils.isEmpty(ids)) {
            return;
        }
        bannerMapper.deleteBatchIds(ids);
        for (Integer id : ids) {
            this.deleteById(id);
        }
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void updateById(Banner banner) {
        validateBanner(banner, true);
        LoginUserInfo loginUserInfo = (LoginUserInfo) SecurityUtils.getSubject().getPrincipal();
        banner.setUpdateTime(new Date());
        banner.setUpdateUser(loginUserInfo.getId());
        bannerMapper.updateById(banner);
    }
    @Override
    public void updateStatus(Banner banner) {
        if (Objects.isNull(banner) || Objects.isNull(banner.getId()) || Objects.isNull(banner.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        LoginUserInfo loginUserInfo = (LoginUserInfo) SecurityUtils.getSubject().getPrincipal();
        banner.setUpdateTime(new Date());
        banner.setUpdateUser(loginUserInfo.getId());
        bannerMapper.updateById(banner);
    }
@@ -76,6 +113,10 @@
        Banner banner = bannerMapper.selectById(id);
        if (Objects.isNull(banner)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        if (StringUtils.isNotBlank(banner.getImgurl())) {
            String path = getBannerPath();
            banner.setImgurlFull(path + banner.getImgurl());
        }
        return banner;
    }
@@ -98,37 +139,20 @@
        QueryWrapper<Banner> queryWrapper = new QueryWrapper<>();
        Utils.MP.blankToNull(pageWrap.getModel());
        pageWrap.getModel().setDeleted(Constants.ZERO);
        if (pageWrap.getModel().getId() != null) {
            queryWrapper.lambda().eq(Banner::getId, pageWrap.getModel().getId());
        }
        if (pageWrap.getModel().getDeleted() != null) {
            queryWrapper.lambda().eq(Banner::getDeleted, pageWrap.getModel().getDeleted());
        }
        if (pageWrap.getModel().getCreateUser() != null) {
            queryWrapper.lambda().eq(Banner::getCreateUser, pageWrap.getModel().getCreateUser());
        }
        if (pageWrap.getModel().getCreateTime() != null) {
            queryWrapper.lambda().ge(Banner::getCreateTime, Utils.Date.getStart(pageWrap.getModel().getCreateTime()));
            queryWrapper.lambda().le(Banner::getCreateTime, Utils.Date.getEnd(pageWrap.getModel().getCreateTime()));
        }
        if (pageWrap.getModel().getUpdateUser() != null) {
            queryWrapper.lambda().eq(Banner::getUpdateUser, pageWrap.getModel().getUpdateUser());
        }
        if (pageWrap.getModel().getUpdateTime() != null) {
            queryWrapper.lambda().ge(Banner::getUpdateTime, Utils.Date.getStart(pageWrap.getModel().getUpdateTime()));
            queryWrapper.lambda().le(Banner::getUpdateTime, Utils.Date.getEnd(pageWrap.getModel().getUpdateTime()));
        }
        if (pageWrap.getModel().getStatus() != null) {
            queryWrapper.lambda().eq(Banner::getStatus, pageWrap.getModel().getStatus());
        }
        if (pageWrap.getModel().getType() != null) {
            queryWrapper.lambda().eq(Banner::getType, pageWrap.getModel().getType());
        }
        if (pageWrap.getModel().getPosition() != null) {
            queryWrapper.lambda().eq(Banner::getPosition, pageWrap.getModel().getPosition());
        }
        if (pageWrap.getModel().getShopId() != null) {
            queryWrapper.lambda().eq(Banner::getShopId, pageWrap.getModel().getShopId());
        if (StringUtils.isNotBlank(pageWrap.getModel().getTitle())) {
            queryWrapper.lambda().like(Banner::getTitle, pageWrap.getModel().getTitle());
        }
        if (pageWrap.getModel().getType() != null) {
            queryWrapper.lambda().eq(Banner::getType, pageWrap.getModel().getType());
        }
        for (PageWrap.SortData sortData : pageWrap.getSorts()) {
            if (sortData.getDirection().equalsIgnoreCase(PageWrap.DESC)) {
@@ -137,7 +161,16 @@
                queryWrapper.orderByAsc(sortData.getProperty());
            }
        }
        return PageData.from(bannerMapper.selectPage(page, queryWrapper));
        PageData<Banner> result = PageData.from(bannerMapper.selectPage(page, queryWrapper));
        if (result != null && result.getRecords() != null) {
            String path = getBannerPath();
            for (Banner b : result.getRecords()) {
                if (StringUtils.isNotBlank(b.getImgurl())) {
                    b.setImgurlFull(path + b.getImgurl());
                }
            }
        }
        return result;
    }
    @Override
@@ -145,4 +178,57 @@
        QueryWrapper<Banner> wrapper = new QueryWrapper<>(banner);
        return bannerMapper.selectCount(wrapper);
    }
    @Override
    public List<Banner> findListByPosition(Integer position) {
        QueryWrapper<Banner> wrapper = new QueryWrapper<>();
        wrapper.lambda()
                .eq(Banner::getDeleted, Constants.ZERO)
                .eq(Banner::getStatus, Constants.ZERO)
                .eq(Banner::getPosition, position)
                .orderByAsc(Banner::getSortnum);
        List<Banner> list = bannerMapper.selectList(wrapper);
        if (list != null && !list.isEmpty()) {
            String path = getBannerPath();
            for (Banner b : list) {
                if (StringUtils.isNotBlank(b.getImgurl())) {
                    b.setImgurlFull(path + b.getImgurl());
                }
            }
        }
        return list;
    }
    private void validateBanner(Banner banner, boolean requireId) {
        if (Objects.isNull(banner)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        if (requireId && Objects.isNull(banner.getId())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        if (StringUtils.isBlank(banner.getTitle())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "标题不能为空");
        }
        if (StringUtils.isBlank(banner.getImgurl())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "图片不能为空");
        }
        if (Objects.isNull(banner.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "状态不能为空");
        }
        if (Objects.isNull(banner.getPosition())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "显示位置不能为空");
        }
        if (Objects.isNull(banner.getType())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "跳转类型不能为空");
        }
        // 跳转类型为1(富文本)或2(外链)时,内容必填
        if (!Constants.equalsInteger(banner.getType(), Constants.ZERO) && StringUtils.isBlank(banner.getContent())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "内容不能为空");
        }
    }
    private String getBannerPath() {
        return systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.RESOURCE_PATH).getCode()
                + systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.BANNER_FILES).getCode();
    }
}
server/services/src/main/java/com/doumee/service/business/impl/CategoryServiceImpl.java
@@ -11,7 +11,9 @@
import com.doumee.core.model.PageWrap;
import com.doumee.core.utils.Utils;
import com.doumee.dao.business.CategoryMapper;
import com.doumee.dao.business.PricingRuleMapper;
import com.doumee.dao.business.model.Category;
import com.doumee.dao.business.model.PricingRule;
import com.doumee.dao.system.model.SystemUser;
import com.doumee.service.business.CategoryService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@@ -42,6 +44,9 @@
    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private PricingRuleMapper pricingRuleMapper;
    @Autowired
    private SystemDictDataBiz systemDictDataBiz;
@@ -242,6 +247,45 @@
        return categoryList;
    }
    @Override
    public List<Category> getCitySizeList(Integer cityId) {
        // 1. 查询该城市已配置的异地寄送规则(pricing_rule type=1),获取fieldA(物品尺寸主键)
        List<PricingRule> rules = pricingRuleMapper.selectList(new QueryWrapper<PricingRule>().lambda()
                .eq(PricingRule::getDeleted, Constants.ZERO)
                .eq(PricingRule::getType, Constants.ONE)
                .eq(PricingRule::getCityId, cityId));
        if (CollectionUtils.isEmpty(rules)) {
            return java.util.Collections.emptyList();
        }
        List<Integer> sizeIds = new java.util.ArrayList<>();
        for (PricingRule rule : rules) {
            if (StringUtils.isNotBlank(rule.getFieldA())) {
                sizeIds.add(Integer.parseInt(rule.getFieldA()));
            }
        }
        if (sizeIds.isEmpty()) {
            return java.util.Collections.emptyList();
        }
        // 2. 查询对应的物品尺寸(category type=4)
        List<Category> sizeList = categoryMapper.selectList(new QueryWrapper<Category>().lambda()
                .eq(Category::getDeleted, Constants.ZERO)
                .eq(Category::getStatus, Constants.ZERO)
                .eq(Category::getType, Constants.FOUR)
                .in(Category::getId, sizeIds)
                .orderByAsc(Category::getSortnum));
        // 3. 拼接图标全路径
        if (!CollectionUtils.isEmpty(sizeList)) {
            String path = systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.RESOURCE_PATH).getCode()
                    + systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.CATEGORY_FILES).getCode();
            for (Category cate : sizeList) {
                if (StringUtils.isNotBlank(cate.getIcon())) {
                    cate.setIconFull(path + cate.getIcon());
                }
            }
        }
        return sizeList;
    }
    private void validateByType(Category category) {
        if (Constants.equalsInteger(category.getType(), Constants.ONE)) {
            if (StringUtils.isBlank(category.getOtherField())) {
server/services/src/main/java/com/doumee/service/business/impl/DriverInfoServiceImpl.java
@@ -17,6 +17,7 @@
import com.doumee.dao.business.MultifileMapper;
import com.doumee.dao.business.SmsrecordMapper;
import com.doumee.dao.business.CategoryMapper;
import com.doumee.biz.system.SystemDictDataBiz;
import com.doumee.dao.business.model.Category;
import com.doumee.dao.business.model.DriverInfo;
import com.doumee.dao.business.model.Member;
@@ -24,12 +25,14 @@
import com.doumee.dao.business.model.Smsrecord;
import com.doumee.dao.vo.AccountResponse;
import com.doumee.dao.dto.AuditDTO;
import com.doumee.dao.dto.ChangeStatusDTO;
import com.doumee.dao.dto.DriverLoginRequest;
import com.doumee.dao.dto.DriverRegisterRequest;
import com.doumee.dao.dto.DriverVerifyRequest;
import com.doumee.service.business.AliSmsService;
import com.doumee.service.business.DriverInfoService;
import com.alibaba.fastjson.JSONObject;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -74,6 +77,9 @@
    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private SystemDictDataBiz systemDictDataBiz;
    @Override
    public Integer create(DriverInfo driverInfo) {
@@ -139,71 +145,41 @@
    @Override
    public PageData<DriverInfo> findPage(PageWrap<DriverInfo> pageWrap) {
        IPage<DriverInfo> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        QueryWrapper<DriverInfo> queryWrapper = new QueryWrapper<>();
        MPJLambdaWrapper<DriverInfo> queryWrapper = new MPJLambdaWrapper<>();
        Utils.MP.blankToNull(pageWrap.getModel());
        pageWrap.getModel().setDeleted(Constants.ZERO);
        if (pageWrap.getModel().getId() != null) {
            queryWrapper.lambda().eq(DriverInfo::getId, pageWrap.getModel().getId());
        // 司机姓名/手机号(关键字模糊查询)
        if (StringUtils.isNotBlank(pageWrap.getModel().getKeyword())) {
            queryWrapper.and(w -> w
                    .like(DriverInfo::getName, pageWrap.getModel().getKeyword())
                    .or()
                    .like(DriverInfo::getTelephone, pageWrap.getModel().getKeyword()));
        }
        if (pageWrap.getModel().getDeleted() != null) {
            queryWrapper.lambda().eq(DriverInfo::getDeleted, pageWrap.getModel().getDeleted());
        // 车牌号
        if (StringUtils.isNotBlank(pageWrap.getModel().getCarCode())) {
            queryWrapper.like(DriverInfo::getCarCode, pageWrap.getModel().getCarCode());
        }
        if (pageWrap.getModel().getCreateUser() != null) {
            queryWrapper.lambda().eq(DriverInfo::getCreateUser, pageWrap.getModel().getCreateUser());
        }
        if (pageWrap.getModel().getCreateTime() != null) {
            queryWrapper.lambda().ge(DriverInfo::getCreateTime, Utils.Date.getStart(pageWrap.getModel().getCreateTime()));
            queryWrapper.lambda().le(DriverInfo::getCreateTime, Utils.Date.getEnd(pageWrap.getModel().getCreateTime()));
        }
        if (pageWrap.getModel().getUpdateUser() != null) {
            queryWrapper.lambda().eq(DriverInfo::getUpdateUser, pageWrap.getModel().getUpdateUser());
        }
        if (pageWrap.getModel().getUpdateTime() != null) {
            queryWrapper.lambda().ge(DriverInfo::getUpdateTime, Utils.Date.getStart(pageWrap.getModel().getUpdateTime()));
            queryWrapper.lambda().le(DriverInfo::getUpdateTime, Utils.Date.getEnd(pageWrap.getModel().getUpdateTime()));
        }
        if (pageWrap.getModel().getRemark() != null) {
            queryWrapper.lambda().eq(DriverInfo::getRemark, pageWrap.getModel().getRemark());
        }
        if (pageWrap.getModel().getName() != null) {
            queryWrapper.lambda().like(DriverInfo::getName, pageWrap.getModel().getName());
        }
        if (pageWrap.getModel().getTelephone() != null) {
            queryWrapper.lambda().like(DriverInfo::getTelephone, pageWrap.getModel().getTelephone());
        }
        if (pageWrap.getModel().getIdcard() != null) {
            queryWrapper.lambda().eq(DriverInfo::getIdcard, pageWrap.getModel().getIdcard());
        }
        if (pageWrap.getModel().getMaritalStatus() != null) {
            queryWrapper.lambda().eq(DriverInfo::getMaritalStatus, pageWrap.getModel().getMaritalStatus());
        }
        if (pageWrap.getModel().getCarType() != null) {
            queryWrapper.lambda().eq(DriverInfo::getCarType, pageWrap.getModel().getCarType());
        }
        if (pageWrap.getModel().getCarCode() != null) {
            queryWrapper.lambda().like(DriverInfo::getCarCode, pageWrap.getModel().getCarCode());
        }
        if (pageWrap.getModel().getCardStartDate() != null) {
            queryWrapper.lambda().ge(DriverInfo::getCardStartDate, Utils.Date.getStart(pageWrap.getModel().getCardStartDate()));
            queryWrapper.lambda().le(DriverInfo::getCardStartDate, Utils.Date.getEnd(pageWrap.getModel().getCardStartDate()));
        }
        if (pageWrap.getModel().getCardEndDate() != null) {
            queryWrapper.lambda().ge(DriverInfo::getCardEndDate, Utils.Date.getStart(pageWrap.getModel().getCardEndDate()));
            queryWrapper.lambda().le(DriverInfo::getCardEndDate, Utils.Date.getEnd(pageWrap.getModel().getCardEndDate()));
        }
        // 状态
        if (pageWrap.getModel().getStatus() != null) {
            queryWrapper.lambda().eq(DriverInfo::getStatus, pageWrap.getModel().getStatus());
            queryWrapper.eq(DriverInfo::getStatus, pageWrap.getModel().getStatus());
        }
        if (pageWrap.getModel().getAuditTime() != null) {
            queryWrapper.lambda().ge(DriverInfo::getAuditTime, Utils.Date.getStart(pageWrap.getModel().getAuditTime()));
            queryWrapper.lambda().le(DriverInfo::getAuditTime, Utils.Date.getEnd(pageWrap.getModel().getAuditTime()));
        // 审批状态
        if (pageWrap.getModel().getAuditStatus() != null) {
            queryWrapper.eq(DriverInfo::getAuditStatus, pageWrap.getModel().getAuditStatus());
        }
        if (pageWrap.getModel().getAuditUser() != null) {
            queryWrapper.lambda().eq(DriverInfo::getAuditUser, pageWrap.getModel().getAuditUser());
        // 创建日期范围
        if (pageWrap.getModel().getCreateTimeStart() != null) {
            queryWrapper.ge(DriverInfo::getCreateTime, Utils.Date.getStart(pageWrap.getModel().getCreateTimeStart()));
        }
        if (pageWrap.getModel().getMemberId() != null) {
            queryWrapper.lambda().eq(DriverInfo::getMemberId, pageWrap.getModel().getMemberId());
        if (pageWrap.getModel().getCreateTimeEnd() != null) {
            queryWrapper.le(DriverInfo::getCreateTime, Utils.Date.getEnd(pageWrap.getModel().getCreateTimeEnd()));
        }
        // 子查询:余额
        queryWrapper.selectAll(DriverInfo.class)
                .select(" ( select ifnull(sum(r.OPT_TYPE * r.AMOUNT),0) from revenue r where r.MEMBER_TYPE = 1 and r.MEMBER_ID= t.id and r.VAILD_STATUS = 1 ) as memberAmount ")
                .selectAs(Category::getName,DriverInfo::getCarTypeName)
                .leftJoin(Category.class, Category::getId,DriverInfo::getCarType);
        for (PageWrap.SortData sortData : pageWrap.getSorts()) {
            if (sortData.getDirection().equalsIgnoreCase(PageWrap.DESC)) {
                queryWrapper.orderByDesc(sortData.getProperty());
@@ -211,7 +187,11 @@
                queryWrapper.orderByAsc(sortData.getProperty());
            }
        }
        return PageData.from(driverInfoMapper.selectPage(page, queryWrapper));
        PageData<DriverInfo> pageData = PageData.from(driverInfoMapper.selectPage(page, queryWrapper));
        for (DriverInfo d : pageData.getRecords()) {
            d.setGender(Constants.getGenderByIdCard(d.getIdcard()));
        }
        return pageData;
    }
    @Override
@@ -315,9 +295,6 @@
            member.setBusinessStatus(Constants.ZERO);
            member.setPassword(secure.encryptPassword(defaultPassword, salt));
            member.setSalt(salt);
            member.setWorkerIdentity(Constants.ZERO);
            member.setDriverIdentity(Constants.ZERO);
            member.setChefIdentity(Constants.ZERO);
            member.setAmount(Constants.ZERO.longValue());
            member.setTotalAmount(Constants.ZERO.longValue());
            member.setStatus(Constants.ZERO);
@@ -329,7 +306,7 @@
            member.setUseIdentity(Constants.ZERO);
            memberMapper.insert(member);
            // 创建司机基础信息(status=0,注册状态)
            // 创建司机基础信息
            DriverInfo driverInfo = new DriverInfo();
            driverInfo.setDeleted(Constants.ZERO);
            driverInfo.setCreateTime(now);
@@ -337,6 +314,7 @@
            driverInfo.setTelephone(telephone);
            driverInfo.setMemberId(member.getId());
            driverInfo.setStatus(Constants.ZERO);
            driverInfo.setAuditStatus(null);
            driverInfoMapper.insert(driverInfo);
        }
@@ -408,10 +386,9 @@
        if (Objects.isNull(driverInfo)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        // 状态校验:status=0(注册)或status=3(审批驳回)可提交认证
        if (driverInfo.getStatus() != null
                && !Constants.equalsInteger(driverInfo.getStatus(), Constants.ZERO)
                && !Constants.equalsInteger(driverInfo.getStatus(), Constants.THREE)) {
        // 状态校验:auditStatus=null(未提交)或auditStatus=2(审批驳回)可提交认证
        if (driverInfo.getAuditStatus() != null
                && !Constants.equalsInteger(driverInfo.getAuditStatus(), Constants.TWO)) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "当前状态不允许提交认证");
        }
        // 根据车辆类型判断是否需要驾驶证
@@ -458,7 +435,7 @@
                .set(DriverInfo::getCardEndDate, request.getCardEndDate())
                .set(DriverInfo::getIdcardImg, request.getIdcardImg())
                .set(DriverInfo::getIdcardImgBack, request.getIdcardImgBack())
                .set(DriverInfo::getStatus, Constants.ONE)
                .set(DriverInfo::getAuditStatus, Constants.ZERO)
                .set(DriverInfo::getUpdateTime, now)
                .set(DriverInfo::getAuditRemark, null)
                .set(DriverInfo::getAuditTime, null)
@@ -533,37 +510,108 @@
        if (Objects.isNull(driverInfo)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        // 只有状态为1(待审批)且已填写认证信息才能审批
        if (!Constants.equalsInteger(driverInfo.getStatus(), Constants.ONE)
        // 只有审批状态为0(待审批)且已填写认证信息才能审批
        if (!Constants.equalsInteger(driverInfo.getAuditStatus(), Constants.ZERO)
                || StringUtils.isBlank(driverInfo.getIdcard())) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "当前状态不允许审批");
        }
        Date now = new Date();
        // 审批结果:auditDTO.auditStatus 0=通过→driverInfo.status=2,1=拒绝→driverInfo.status=3
        Integer newStatus;
        // 审批结果:auditDTO.auditStatus 0=通过→auditStatus=1,1=拒绝→auditStatus=2
        Integer newAuditStatus;
        if (Constants.equalsInteger(auditDTO.getAuditStatus(), Constants.ZERO)) {
            newStatus = Constants.TWO;  // 审批通过
            newAuditStatus = Constants.ONE;  // 审批通过
            // 审批通过时司机定级为必填
            if (auditDTO.getDriverLevel() == null || auditDTO.getDriverLevel() < 1 || auditDTO.getDriverLevel() > 5) {
                throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "审批通过时必须填写司机定级");
            }
        } else if (Constants.equalsInteger(auditDTO.getAuditStatus(), Constants.ONE)) {
            newStatus = Constants.THREE;  // 审批驳回
            newAuditStatus = Constants.TWO;  // 审批驳回
        } else {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "审批状态参数错误");
        }
        // 更新司机状态
        // 更新司机审批状态
        driverInfoMapper.update(new UpdateWrapper<DriverInfo>().lambda()
                .set(DriverInfo::getStatus, newStatus)
                .set(DriverInfo::getAuditStatus, newAuditStatus)
                .set(DriverInfo::getAuditTime, now)
                .set(DriverInfo::getAuditRemark, auditDTO.getAuditRemark())
                .set(DriverInfo::getAuditUser, auditDTO.getAuditUser())
                .set(auditDTO.getDriverLevel() != null, DriverInfo::getDriverLevel, auditDTO.getDriverLevel())
                .set(DriverInfo::getUpdateTime, now)
                .eq(DriverInfo::getId, auditDTO.getId()));
        // 更新会员司机认证状态:通过=2,驳回=3
        Integer driverStatus = Constants.equalsInteger(newStatus, Constants.TWO) ? Constants.TWO : Constants.THREE;
        Integer driverStatus = Constants.equalsInteger(newAuditStatus, Constants.ONE) ? Constants.TWO : Constants.THREE;
        memberMapper.update(new UpdateWrapper<Member>().lambda()
                .set(Member::getBusinessStatus, driverStatus)
                .set(Member::getUpdateTime, now)
                .eq(Member::getId, driverInfo.getMemberId()));
    }
    @Override
    public DriverInfo getDetail(Integer id) {
        DriverInfo driverInfo = driverInfoMapper.selectById(id);
        if (Objects.isNull(driverInfo) || Constants.equalsInteger(driverInfo.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        // 拼接图片前缀
        String imgPrefix = "";
        try {
            imgPrefix = systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.RESOURCE_PATH).getCode()
                    + systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.DRIVER_FILES).getCode();
        } catch (Exception e) {
            // 未配置时忽略
        }
        driverInfo.setImgPrefix(imgPrefix);
        driverInfo.setGender(Constants.getGenderByIdCard(driverInfo.getIdcard()));
        // 查询车辆类型名称和是否需要驾驶证
        if (driverInfo.getCarType() != null) {
            Category category = categoryMapper.selectById(driverInfo.getCarType());
            if (Objects.nonNull(category)) {
                driverInfo.setCarTypeName(category.getName());
                driverInfo.setNeedLicense(Constants.equalsInteger(Integer.valueOf(category.getOtherField()), Constants.ONE) ? Constants.ONE : Constants.ZERO);
            }
        }
        // 查询照片列表:objType=6车辆照片、7驾驶证照片、8其他资料照片
        List<Multifile> multifileList = multifileMapper.selectList(new QueryWrapper<Multifile>().lambda()
                .eq(Multifile::getObjId, driverInfo.getId())
                .in(Multifile::getObjType, 6, 7, 8)
                .orderByAsc(Multifile::getSortnum));
        if (!CollectionUtils.isEmpty(multifileList)) {
            for (Multifile mf : multifileList) {
                mf.setFileurlFull(imgPrefix + mf.getFileurl());
                if (Constants.equalsInteger(mf.getObjType(), 6)) {
                    driverInfo.getCarImgList().add(mf);
                } else if (Constants.equalsInteger(mf.getObjType(), 7)) {
                    driverInfo.getLicenseImgList().add(mf);
                } else if (Constants.equalsInteger(mf.getObjType(), 8)) {
                    driverInfo.getOtherImgList().add(mf);
                }
            }
        }
        return driverInfo;
    }
    @Override
    public void changeStatus(ChangeStatusDTO dto) {
        if (dto.getId() == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "主键不能为空");
        }
        if (dto.getStatus() == null || (dto.getStatus() != 0 && dto.getStatus() != 1)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "状态参数错误,0=启用;1=禁用");
        }
        DriverInfo driverInfo = driverInfoMapper.selectById(dto.getId());
        if (Objects.isNull(driverInfo) || Constants.equalsInteger(driverInfo.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        driverInfo.setStatus(dto.getStatus());
        driverInfo.setUpdateTime(new Date());
        driverInfoMapper.updateById(driverInfo);
        // 联动修改会员状态(member主键与driver_info主键一致)
        memberMapper.update(new UpdateWrapper<Member>().lambda()
                .set(Member::getStatus, dto.getStatus())
                .set(Member::getUpdateTime, new Date())
                .eq(Member::getId, driverInfo.getId()));
    }
    /**
     * 批量保存附件记录
     */
server/services/src/main/java/com/doumee/service/business/impl/IdentityInfoServiceImpl.java
@@ -104,11 +104,6 @@
            identityInfo.setAuditStatus(Constants.ONE);
            identityInfoMapper.insert(identityInfo);
            memberMapper.update(new UpdateWrapper<Member>().lambda()
                    .set(Constants.equalsInteger(identityInfo.getType(),Constants.ZERO),Member::getWorkerIdentity,Constants.ONE)
                    .set(Constants.equalsInteger(identityInfo.getType(),Constants.ONE),Member::getDriverIdentity,Constants.ONE)
                    .set(Constants.equalsInteger(identityInfo.getType(),Constants.TWO),Member::getChefIdentity,Constants.ONE)
                    .eq(Member::getId,identityInfo.getMemberId()));
        return identityInfo.getId();
    }
@@ -166,11 +161,6 @@
        identityInfo.setUpdateTime(new Date());
        identityInfo.setAuditStatus(Constants.ONE);
        identityInfoMapper.updateById(identityInfo);
        memberMapper.update(new UpdateWrapper<Member>().lambda()
                .set(Constants.equalsInteger(identityInfo.getType(),Constants.ZERO),Member::getWorkerIdentity,Constants.ONE)
                .set(Constants.equalsInteger(identityInfo.getType(),Constants.ONE),Member::getDriverIdentity,Constants.ONE)
                .set(Constants.equalsInteger(identityInfo.getType(),Constants.TWO),Member::getChefIdentity,Constants.ONE)
                .eq(Member::getId,identityInfo.getMemberId()));
    }
@@ -289,15 +279,9 @@
        String path  = systemDictDataBiz.queryByCode(Constants.SYSTEM,Constants.RESOURCE_PATH).getCode()
                +systemDictDataBiz.queryByCode(Constants.SYSTEM,Constants.INENTITY_FILES).getCode();
        //类型:0=用工身份;1=货运身份;2=供餐身份;
        if(!Constants.equalsInteger(member.getWorkerIdentity(),Constants.ZERO)){//用工身份
            addMemberModelByType(member.getId(),Constants.ZERO,list,path);
        }
        if(!Constants.equalsInteger(member.getDriverIdentity(),Constants.ZERO)){//货运身份
            addMemberModelByType(member.getId(),Constants.ONE,list,path);
        }
        if(!Constants.equalsInteger(member.getChefIdentity(),Constants.ZERO)){//供餐很粉
            addMemberModelByType(member.getId(),Constants.TWO,list,path);
        }
        addMemberModelByType(member.getId(),Constants.ZERO,list,path);
        addMemberModelByType(member.getId(),Constants.ONE,list,path);
        addMemberModelByType(member.getId(),Constants.TWO,list,path);
        return list;
    }
@@ -474,10 +458,6 @@
        memberMapper.update(new UpdateWrapper<Member>().lambda()
                .set(Member::getUpdateUser,user.getId())
                .set(Member::getUpdateTime,date)
                .set(Constants.equalsInteger(auditDTO.getAuditStatus(),Constants.ZERO),Member::getAutoReceiveStatus,Constants.ONE)
                .set(Constants.equalsInteger(identityInfo.getType(),Constants.ZERO),Member::getWorkerIdentity, auditDTO.getAuditStatus() )
                .set(Constants.equalsInteger(identityInfo.getType(),Constants.ONE),Member::getDriverIdentity , auditDTO.getAuditStatus() )
                .set(Constants.equalsInteger(identityInfo.getType(),Constants.TWO),Member::getChefIdentity , auditDTO.getAuditStatus() )
                .eq(Member::getId,identityInfo.getMemberId())
        );
server/services/src/main/java/com/doumee/service/business/impl/MemberServiceImpl.java
@@ -14,11 +14,19 @@
import com.doumee.core.utils.Utils;
import com.doumee.dao.business.IdentityInfoMapper;
import com.doumee.dao.business.MemberMapper;
import com.doumee.dao.business.ShopInfoMapper;
import com.doumee.dao.business.SmsrecordMapper;
import com.doumee.dao.business.model.IdentityInfo;
import com.doumee.dao.business.model.Member;
import com.doumee.dao.business.model.ShopInfo;
import com.doumee.dao.business.model.MemberRevenue;
import com.doumee.dao.business.model.Smsrecord;
import com.doumee.dao.dto.MemberListQueryDTO;
import com.doumee.dao.dto.UpdMobileRequest;
import com.doumee.dao.dto.WxPhoneRequest;
import com.doumee.dao.vo.AccountResponse;
import com.doumee.dao.vo.MemberDetailVO;
import com.doumee.dao.vo.MemberListVO;
import com.doumee.dao.vo.UserCenterVO;
import com.doumee.service.business.MemberService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@@ -59,6 +67,9 @@
    private IdentityInfoMapper identityInfoMapper;
    @Autowired
    private SmsrecordMapper smsrecordMapper;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
@@ -66,6 +77,9 @@
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    @Autowired
    private ShopInfoMapper shopInfoMapper;
    @Override
@@ -178,15 +192,6 @@
        if (pageWrap.getModel().getName() != null) {
            queryWrapper.lambda().like(Member::getName, pageWrap.getModel().getName());
        }
        if (pageWrap.getModel().getWorkerIdentity() != null) {
            queryWrapper.lambda().eq(Member::getWorkerIdentity, pageWrap.getModel().getWorkerIdentity());
        }
        if (pageWrap.getModel().getDriverIdentity() != null) {
            queryWrapper.lambda().eq(Member::getDriverIdentity, pageWrap.getModel().getDriverIdentity());
        }
        if (pageWrap.getModel().getChefIdentity() != null) {
            queryWrapper.lambda().eq(Member::getChefIdentity, pageWrap.getModel().getChefIdentity());
        }
        if (pageWrap.getModel().getAmount() != null) {
            queryWrapper.lambda().eq(Member::getAmount, pageWrap.getModel().getAmount());
        }
@@ -224,12 +229,6 @@
        if (pageWrap.getModel().getArea() != null) {
            queryWrapper.lambda().eq(Member::getArea, pageWrap.getModel().getArea());
        }
        //如果查询接单方
        queryWrapper.lambda().and(Constants.equalsInteger(pageWrap.getModel().getType(),Constants.ONE ),w1->{
            w1.eq(Member::getWorkerIdentity,Constants.TWO )
                    .or().eq(Member::getChefIdentity,Constants.TWO )
                    .or().eq(Member::getDriverIdentity,Constants.TWO );
        });
        queryWrapper.lambda().ge(pageWrap.getModel().getStartTime()!=null, Member::getCreateTime,pageWrap.getModel().getStartTime());
        queryWrapper.lambda().le(pageWrap.getModel().getEndTime()!=null,Member::getCreateTime,pageWrap.getModel().getEndTime());
@@ -270,8 +269,10 @@
            if (StringUtils.isBlank(openId)) {
                throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "获取openid失败!请联系管理员");
            }
            Member member = memberMapper.selectOne(new QueryWrapper<Member>().lambda().eq(Member::getOpenid, openId).eq(Member::getUserType,Constants.ZERO)
                    .eq(Member::getDeleted, Constants.ZERO).ne(Member::getStatus, Constants.TWO).last("limit 1"));
            Member member = memberMapper.selectOne(new QueryWrapper<Member>().lambda().eq(Member::getOpenid, openId)
                    .ne(Member::getUserType,Constants.ONE)
                    .eq(Member::getDeleted, Constants.ZERO)
                    .ne(Member::getStatus, Constants.TWO).last("limit 1"));
            AccountResponse accountResponse = new AccountResponse();
            accountResponse.setOpenid(openId);
            if(Objects.isNull(member)){
@@ -283,13 +284,13 @@
            String token = JwtTokenUtil.generateTokenForRedis(member.getId(), Constants.ZERO, JSONObject.toJSONString(member), redisTemplate);
            accountResponse.setToken(token);
            accountResponse.setMember(member);
            // 门店用户身份时,返回申请的门店状态
            fillShopInfo(accountResponse, member);
            return accountResponse;
        } catch (WxErrorException e) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "微信登录异常!请联系管理员");
        }
    }
    @Override
@@ -305,7 +306,9 @@
            if(Objects.isNull(mobile)){
                throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"未获取到手机号");
            }
            Member member =  memberMapper.selectOne(new QueryWrapper<Member>().lambda().eq(Member::getTelephone,mobile).eq(Member::getUserType,Constants.ZERO)
            Member member =  memberMapper.selectOne(new QueryWrapper<Member>().lambda()
                    .eq(Member::getTelephone,mobile)
                    .ne(Member::getUserType,Constants.ONE)
                    .ne(Member::getStatus, Constants.TWO).last("limit 1"));
            if(Objects.nonNull(member)){
                if(!Constants.equalsInteger(member.getStatus(),Constants.ZERO)){
@@ -322,9 +325,6 @@
                member.setTelephone(mobile);
                member.setNickName(mobile);
//                member.setCoverImage("1.png");
                member.setWorkerIdentity(Constants.ZERO);
                member.setDriverIdentity(Constants.ZERO);
                member.setChefIdentity(Constants.ZERO);
                member.setAmount(Constants.ZERO.longValue());
                member.setTotalAmount(Constants.ZERO.longValue());
                member.setStatus(Constants.ZERO);
@@ -340,6 +340,8 @@
            AccountResponse accountResponse = new AccountResponse();
            accountResponse.setToken(token);
            accountResponse.setMember(member);
            // 门店用户身份时,返回申请的门店状态
            fillShopInfo(accountResponse, member);
            return accountResponse;
        } catch (Exception e) {
            e.printStackTrace();
@@ -347,6 +349,21 @@
        }
    }
    /**
     * 门店用户身份时,填充门店审核状态
     */
    private void fillShopInfo(AccountResponse response, Member member) {
        if (Constants.TWO.equals(member.getUserType())) {
            ShopInfo shopInfo = shopInfoMapper.selectOne(new QueryWrapper<ShopInfo>().lambda()
                    .eq(ShopInfo::getRegionMemberId, member.getId())
                    .eq(ShopInfo::getDeleted, Constants.ZERO)
                    .last("limit 1"));
            if (shopInfo != null) {
                response.setShopId(shopInfo.getId());
                response.setShopAuditStatus(shopInfo.getAuditStatus());
            }
        }
    }
    @Override
@@ -435,10 +452,7 @@
        userCenterVO.setAboutUs(StringUtils.trimToNull(systemDictDataBiz.queryByCode(Constants.SYSTEM,Constants.ABOUT_US).getCode()));
        userCenterVO.setUserAgreement(StringUtils.trimToNull(systemDictDataBiz.queryByCode(Constants.SYSTEM,Constants.USER_AGREEMENT).getCode()));
        userCenterVO.setPrivacyAgreement(StringUtils.trimToNull(systemDictDataBiz.queryByCode(Constants.SYSTEM,Constants.PRIVACY_AGREEMENT).getCode()));
//        userCenterVO.setFeeStandards(StringUtils.trimToNull(systemDictDataBiz.queryByCode(Constants.SYSTEM,Constants.FEE_STANDARDS).getCode()));
//        userCenterVO.setServerIntroduce(StringUtils.trimToNull(systemDictDataBiz.queryByCode(Constants.SYSTEM,Constants.SERVER_INTRODUCE).getCode()));
//        userCenterVO.setServerPhone(StringUtils.trimToNull(systemDictDataBiz.queryByCode(Constants.SYSTEM,Constants.SERVER_PHONE).getCode()));
        userCenterVO.setServerIntroduce(StringUtils.trimToNull(systemDictDataBiz.queryByCode(Constants.SYSTEM,Constants.SERVER_INTRODUCE).getCode()));
        return userCenterVO;
    }
@@ -462,4 +476,156 @@
        redisTemplate.delete(token);
    }
    /***************管理端会员列表接口*************/
    @Override
    public PageData<MemberListVO> findMemberListPage(PageWrap<MemberListQueryDTO> pageWrap) {
        IPage<Member> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        MemberListQueryDTO query = pageWrap.getModel();
        QueryWrapper<Member> queryWrapper = new QueryWrapper<>();
        queryWrapper.lambda().eq(Member::getDeleted, Constants.ZERO);
        queryWrapper.lambda().eq(Member::getUserType, Constants.ZERO);
        if (query != null) {
            if (StringUtils.isNotBlank(query.getKeyword())) {
                queryWrapper.lambda().and(w -> w
                        .like(Member::getName, query.getKeyword())
                        .or().like(Member::getTelephone, query.getKeyword()));
            }
            if (query.getStatus() != null) {
                queryWrapper.lambda().eq(Member::getStatus, query.getStatus());
            }
            if (query.getStartTime() != null) {
                queryWrapper.lambda().ge(Member::getCreateTime, query.getStartTime());
            }
            if (query.getEndTime() != null) {
                queryWrapper.lambda().le(Member::getCreateTime, Utils.Date.getEnd(query.getEndTime()));
            }
        }
        for (PageWrap.SortData sortData : pageWrap.getSorts()) {
            if (sortData.getDirection().equalsIgnoreCase(PageWrap.DESC)) {
                queryWrapper.orderByDesc(sortData.getProperty());
            } else {
                queryWrapper.orderByAsc(sortData.getProperty());
            }
        }
        IPage<Member> memberPage = memberMapper.selectPage(page, queryWrapper);
        IPage<MemberListVO> voPage = memberPage.convert(this::toListVO);
        return PageData.from(voPage);
    }
    @Override
    public MemberDetailVO findMemberDetail(Integer id) {
        Member member = memberMapper.selectById(id);
        if (Objects.isNull(member) || Constants.equalsInteger(member.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        MemberDetailVO detail = new MemberDetailVO();
        detail.setId(member.getId());
        detail.setNickName(member.getNickName());
        detail.setName(member.getName());
        detail.setTelephone(member.getTelephone());
        detail.setAmount(member.getAmount());
        detail.setTotalAmount(member.getTotalAmount());
        detail.setStatus(member.getStatus());
        detail.setUseIdentity(member.getUseIdentity());
        detail.setOpenid(member.getOpenid());
        detail.setUserType(member.getUserType());
        detail.setBusinessStatus(member.getBusinessStatus());
        detail.setScore(member.getScore());
        detail.setReceiveNum(member.getReceiveNum());
        detail.setPublishNum(member.getPublishNum());
        detail.setCreateTime(member.getCreateTime());
        detail.setLoginTime(member.getLoginTime());
        detail.setLoginTimes(member.getLoginTimes());
        detail.setProvince(member.getProvince());
        detail.setCity(member.getCity());
        detail.setArea(member.getArea());
        detail.setAutoReceiveStatus(member.getAutoReceiveStatus());
        if (StringUtils.isNotBlank(member.getCoverImage())) {
            String path = systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.RESOURCE_PATH).getCode()
                    + systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.MEMBER_FILES).getCode();
            detail.setFullCoverImage(path + member.getCoverImage());
        }
        return detail;
    }
    private MemberListVO toListVO(Member member) {
        MemberListVO vo = new MemberListVO();
        vo.setId(member.getId());
        vo.setNickName(member.getNickName());
        vo.setName(member.getName());
        vo.setTelephone(member.getTelephone());
        vo.setCreateTime(member.getCreateTime());
        vo.setStatus(member.getStatus());
        return vo;
    }
    @Override
    public void verifyUserPhone(UpdMobileRequest request){
        if(Objects.isNull(request)
                || Objects.isNull(request.getMemberId())
                || StringUtils.isBlank(request.getPhone())
                || StringUtils.isBlank(request.getCode())
        ){
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        Member member = memberMapper.selectById(request.getMemberId());
        if(StringUtils.isNotBlank(member.getTelephone())&&!member.getTelephone().equals(request.getPhone())){
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"手机号错误");
        }
        this.verifyPhoneCode(request.getCode(),request.getPhone());
    }
    public void verifyPhoneCode(String code,String phone){
        //验证码
        Smsrecord smsrecord = smsrecordMapper.selectOne(new QueryWrapper<Smsrecord>().lambda()
                .eq(Smsrecord::getDeleted,Constants.ZERO)
                .eq(Smsrecord::getCode,code)
                .eq(Smsrecord::getPhone,phone)
                .last(" limit 1")
        );
        if(Objects.isNull(smsrecord)){
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"验证码错误!");
        }
        if(!Constants.equalsInteger(smsrecord.getStatus(),Constants.ZERO)){
            if(Constants.equalsInteger(smsrecord.getStatus(),Constants.ONE)){
                throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"验证码已使用");
            }else{
                throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"验证码已过期");
            }
        }
        smsrecordMapper.update(new UpdateWrapper<Smsrecord>().lambda()
                .set(Smsrecord::getStatus,Constants.ONE)
                .eq(Smsrecord::getId,smsrecord.getId())
        );
    }
    @Override
    public void updateUserPhone(UpdMobileRequest request){
        if(Objects.isNull(request)
                || Objects.isNull(request.getMemberId())
                || StringUtils.isBlank(request.getPhone())
                || StringUtils.isBlank(request.getCode())
        ){
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        //判断新手机号是否已被使用
        if(memberMapper.selectCount(new QueryWrapper<Member>().lambda().eq(Member::getTelephone,request.getPhone()))>Constants.ZERO){
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"对不起,新手机号已被绑定,无法进行换绑!");
        }
        Member member = memberMapper.selectById(request.getMemberId());
        if(StringUtils.isNotBlank(member.getTelephone())&&member.getTelephone().equals(request.getPhone())){
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"无法更换相同手机号");
        }
        this.verifyPhoneCode(request.getCode(),request.getPhone());
        memberMapper.update(new UpdateWrapper<Member>().lambda()
                .set(Member::getTelephone,request.getPhone())
                .eq(Member::getId,request.getMemberId())
        );
    }
}
server/services/src/main/java/com/doumee/service/business/impl/OrdersServiceImpl.java
@@ -4,23 +4,54 @@
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.doumee.biz.system.AreasBiz;
import com.doumee.biz.system.OperationConfigBiz;
import com.doumee.biz.system.SystemDictDataBiz;
import com.doumee.config.wx.WxMiniConfig;
import com.doumee.config.wx.WxMiniUtilService;
import com.doumee.core.constants.Constants;
import com.doumee.core.constants.ResponseStatus;
import com.doumee.core.exception.BusinessException;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.core.utils.DateUtil;
import com.doumee.core.utils.Tencent.MapUtil;
import com.doumee.core.utils.Utils;
import com.doumee.dao.business.OrdersMapper;
import com.doumee.dao.business.model.Category;
import com.doumee.dao.business.model.Orders;
import com.doumee.dao.business.*;
import com.doumee.dao.business.model.*;
import com.doumee.dao.system.SystemUserMapper;
import com.doumee.dao.system.model.SystemUser;
import com.doumee.dao.dto.CalculateLocalPriceDTO;
import com.doumee.dao.dto.CalculateRemotePriceDTO;
import com.doumee.dao.dto.CreateOrderDTO;
import com.doumee.dao.dto.DispatchDTO;
import com.doumee.dao.dto.MyOrderDTO;
import com.doumee.dao.dto.OrderItemDTO;
import com.doumee.dao.vo.*;
import com.doumee.service.business.OrderLogService;
import com.doumee.service.business.OrdersService;
import com.github.binarywang.wxpay.bean.request.BaseWxPayRequest;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.xiaoymin.knife4j.core.util.CollectionUtils;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.List;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
 * 寄存订单信息Service实现
@@ -32,6 +63,50 @@
    @Autowired
    private OrdersMapper ordersMapper;
    @Autowired
    private MemberMapper memberMapper;
    @Autowired
    private ShopInfoMapper shopInfoMapper;
    @Autowired
    private DriverInfoMapper driverInfoMapper;
    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private MultifileMapper multifileMapper;
    @Autowired
    private OrdersDetailMapper ordersDetailMapper;
    @Autowired
    private SystemDictDataBiz systemDictDataBiz;
    @Autowired
    private OrderLogService orderLogService;
    @Autowired
    private OrdersRefundMapper ordersRefundMapper;
    @Autowired
    private WxMiniUtilService wxMiniUtilService;
    @Autowired
    private SystemUserMapper systemUserMapper;
    @Autowired
    private PricingRuleMapper pricingRuleMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private AreasBiz areasBiz;
    @Autowired
    private OperationConfigBiz operationConfigBiz;
    @Override
    public Integer create(Orders orders) {
@@ -102,11 +177,18 @@
        MPJLambdaWrapper<Orders> queryWrapper = new MPJLambdaWrapper<Orders>()
                .selectAll(Orders.class)
                .selectAs(Category::getDetail, Orders::getOrderLevel)
                .leftJoin(Category.class, Category::getId, Orders::getGoodType);
                .select("s1.name", Orders::getDepositShopName)
                .leftJoin(Category.class, Category::getId, Orders::getGoodType)
                .leftJoin(DriverInfo.class, DriverInfo::getId, Orders::getAcceptDriver)
                .leftJoin("shop_info s1 on s1.id = t.DEPOSIT_SHOP_ID")
                .leftJoin("shop_info s2 on s2.id = t.TAKE_SHOP_ID");
                ;
        Utils.MP.blankToNull(pageWrap.getModel());
        pageWrap.getModel().setDeleted(Constants.ZERO);
        queryWrapper.eq(pageWrap.getModel().getDeleted() != null, Orders::getDeleted, pageWrap.getModel().getDeleted());
        queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getCode()), Orders::getCode, pageWrap.getModel().getCode());
        queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getDepositShopName()), "s1.name", pageWrap.getModel().getDepositShopName());
        queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getTakeShopName()),  "s2.name",  pageWrap.getModel().getTakeShopName());
        queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getGoodsInfo()), Orders::getGoodsInfo, pageWrap.getModel().getGoodsInfo());
        queryWrapper.ge(pageWrap.getModel().getCreateStartTime() != null, Orders::getCreateTime, Utils.Date.getStart(pageWrap.getModel().getCreateStartTime()));
        queryWrapper.le(pageWrap.getModel().getCreateEndTime() != null, Orders::getCreateTime, Utils.Date.getEnd(pageWrap.getModel().getCreateEndTime()));
@@ -114,6 +196,8 @@
        queryWrapper.eq(pageWrap.getModel().getType() != null, Orders::getType, pageWrap.getModel().getType());
        queryWrapper.eq(pageWrap.getModel().getStatus() != null, Orders::getStatus, pageWrap.getModel().getStatus());
        queryWrapper.eq(pageWrap.getModel().getTakeShopId() != null, Orders::getTakeShopId, pageWrap.getModel().getTakeShopId());
        queryWrapper.and(pageWrap.getModel().getDriverKeyword() != null, i->i.like(DriverInfo::getName, pageWrap.getModel().getDriverKeyword())
                .or().like(DriverInfo::getTelephone, pageWrap.getModel().getDriverKeyword()));
        for (PageWrap.SortData sortData : pageWrap.getSorts()) {
            if (sortData.getDirection().equalsIgnoreCase(PageWrap.DESC)) {
                queryWrapper.orderByDesc(sortData.getProperty());
@@ -125,9 +209,1847 @@
    }
    @Override
    public OrderSummaryVO findSummary(PageWrap<Orders> pageWrap) {
        // 构建与findPage相同的查询条件
        MPJLambdaWrapper<Orders> queryWrapper = new MPJLambdaWrapper<Orders>()
                .leftJoin(Category.class, Category::getId, Orders::getGoodType)
                .leftJoin(DriverInfo.class, DriverInfo::getId, Orders::getAcceptDriver)
                .leftJoin("shop_info s1 on s1.id = t.DEPOSIT_SHOP_ID")
                .leftJoin("shop_info s2 on s2.id = t.TAKE_SHOP_ID");
        Utils.MP.blankToNull(pageWrap.getModel());
        pageWrap.getModel().setDeleted(Constants.ZERO);
        queryWrapper.eq(pageWrap.getModel().getDeleted() != null, Orders::getDeleted, pageWrap.getModel().getDeleted());
        queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getCode()), Orders::getCode, pageWrap.getModel().getCode());
        queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getDepositShopName()), "s1.name", pageWrap.getModel().getDepositShopName());
        queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getTakeShopName()),  "s2.name",  pageWrap.getModel().getTakeShopName());
        queryWrapper.like(StringUtils.isNotBlank(pageWrap.getModel().getGoodsInfo()), Orders::getGoodsInfo, pageWrap.getModel().getGoodsInfo());
        queryWrapper.ge(pageWrap.getModel().getCreateStartTime() != null, Orders::getCreateTime, Utils.Date.getStart(pageWrap.getModel().getCreateStartTime()));
        queryWrapper.le(pageWrap.getModel().getCreateEndTime() != null, Orders::getCreateTime, Utils.Date.getEnd(pageWrap.getModel().getCreateEndTime()));
        queryWrapper.eq(pageWrap.getModel().getDepositShopId() != null, Orders::getDepositShopId, pageWrap.getModel().getDepositShopId());
        queryWrapper.eq(pageWrap.getModel().getType() != null, Orders::getType, pageWrap.getModel().getType());
        queryWrapper.eq(pageWrap.getModel().getStatus() != null, Orders::getStatus, pageWrap.getModel().getStatus());
        queryWrapper.eq(pageWrap.getModel().getTakeShopId() != null, Orders::getTakeShopId, pageWrap.getModel().getTakeShopId());
        queryWrapper.and(pageWrap.getModel().getDriverKeyword() != null, i->i.like(DriverInfo::getName, pageWrap.getModel().getDriverKeyword())
                .or().like(DriverInfo::getTelephone, pageWrap.getModel().getDriverKeyword()));
        queryWrapper.select(
                "IFNULL(SUM(t.total_amount), 0) as total_amount_sum",
                "IFNULL(SUM(CASE WHEN t.settlement_status = 1 THEN t.total_amount ELSE 0 END), 0) as settled_total_amount_sum",
                "IFNULL(SUM(t.driver_fee), 0) as driver_fee_sum",
                "IFNULL(SUM(CASE WHEN t.settlement_status = 1 THEN t.driver_fee ELSE 0 END), 0) as settled_driver_fee_sum"
        );
        queryWrapper.groupBy("1=1");
        List<Map<String, Object>> result = ordersMapper.selectJoinMaps(queryWrapper);
        OrderSummaryVO vo = new OrderSummaryVO();
        if (result != null && !result.isEmpty()) {
            Map<String, Object> row = result.get(0);
            vo.setTotalAmountSum(toLong(row.get("total_amount_sum")));
            vo.setSettledTotalAmountSum(toLong(row.get("settled_total_amount_sum")));
            vo.setDriverFeeSum(toLong(row.get("driver_fee_sum")));
            vo.setSettledDriverFeeSum(toLong(row.get("settled_driver_fee_sum")));
        } else {
            vo.setTotalAmountSum(0L);
            vo.setSettledTotalAmountSum(0L);
            vo.setDriverFeeSum(0L);
            vo.setSettledDriverFeeSum(0L);
        }
        return vo;
    }
    private Long toLong(Object val) {
        if (val == null) return 0L;
        if (val instanceof Number) return ((Number) val).longValue();
        return Long.parseLong(val.toString());
    }
    @Override
    public BigDecimal calculateInsuranceFee(BigDecimal declaredValue) {
        if (declaredValue == null || declaredValue.compareTo(BigDecimal.ZERO) <= 0) {
            return BigDecimal.ZERO;
        }
        String rateStr = systemDictDataBiz.queryByCode(Constants.OPERATION_CONFIG, Constants.OP_INSURANCE_RATE).getCode();
        BigDecimal rate = new BigDecimal(rateStr);
        return declaredValue.multiply(rate).setScale(2, BigDecimal.ROUND_HALF_UP);
    }
    /**
     * 计算就地存取预估费用
     *
     * 计算规则:
     * 1. 根据城市+物品类型 查询 pricing_rule(type=0),fieldA=categoryId, fieldB=单价(分/天)
     * 2. 每项小计 = 单价 × 数量 × 天数
     * 3. 物品价格 = 各项小计之和
     * 4. 保价费用 = 报价金额 × 保价费率(字典 INSURANCE_RATE),元转分
     * 5. 总价格 = 物品价格 + 保价费用
     *
     * @param dto 就地存取计价请求参数
     * @return 价格计算结果
     */
    @Override
    public PriceCalculateVO calculateLocalPrice(CalculateLocalPriceDTO dto) {
        // 天数校验,最少1天
        int days = dto.getEstimatedDepositDays() != null && dto.getEstimatedDepositDays() > 0
                ? dto.getEstimatedDepositDays() : 1;
        // 收集所有物品类型ID
        List<Integer> categoryIds = new ArrayList<>();
        for (OrderItemDTO item : dto.getItems()) {
            categoryIds.add(item.getCategoryId());
        }
        // 批量查询计价规则 pricing_rule type=0:fieldA=categoryId, fieldB=单价(分/天)
        List<String> fieldAList = new ArrayList<>();
        for (Integer cid : categoryIds) {
            fieldAList.add(String.valueOf(cid));
        }
        List<PricingRule> rules = pricingRuleMapper.selectList(new QueryWrapper<PricingRule>().lambda()
                .eq(PricingRule::getDeleted, Constants.ZERO)
                .eq(PricingRule::getType, Constants.ZERO)
                .eq(PricingRule::getCityId, dto.getCityId())
                .in(PricingRule::getFieldA, fieldAList));
        Map<String, PricingRule> ruleMap = new HashMap<>();
        for (PricingRule r : rules) {
            ruleMap.put(r.getFieldA(), r);
        }
        // 批量查询物品类型名称
        List<Category> categories = categoryMapper.selectBatchIds(categoryIds);
        Map<Integer, String> categoryNameMap = new HashMap<>();
        Map<Integer, String> categoryDetailMap = new HashMap<>();
        for (Category c : categories) {
            categoryNameMap.put(c.getId(), c.getName());
            categoryDetailMap.put(c.getId(), c.getDetail());
        }
        // 计算每项物品费用:小计 = 单价 × 数量 × 天数
        List<ItemPriceVO> itemList = new ArrayList<>();
        long itemPriceTotal = 0L;
        for (OrderItemDTO item : dto.getItems()) {
            PricingRule rule = ruleMap.get(String.valueOf(item.getCategoryId()));
            if (rule == null) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(),
                        "未找到该城市物品类型的计价规则");
            }
            long unitPrice = Long.parseLong(rule.getFieldB());
            long subtotal = unitPrice * item.getQuantity() * days;
            ItemPriceVO vo = new ItemPriceVO();
            vo.setCategoryId(item.getCategoryId());
            vo.setCategoryName(categoryNameMap.getOrDefault(item.getCategoryId(), ""));
            vo.setDetail(categoryDetailMap.get(item.getCategoryId()));
            vo.setQuantity(item.getQuantity());
            vo.setUnitPrice(unitPrice);
            vo.setLocallyPrice(unitPrice);
            vo.setSubtotal(subtotal);
            itemList.add(vo);
            itemPriceTotal += subtotal;
        }
        // 保价费用:报价金额 × 保价费率(字典 INSURANCE_RATE),元→分
        long insuranceFeeFen = 0L;
        if (Boolean.TRUE.equals(dto.getInsured()) && dto.getDeclaredAmount() != null) {
            BigDecimal insuranceFeeYuan = calculateInsuranceFee(dto.getDeclaredAmount());
            insuranceFeeFen = insuranceFeeYuan.multiply(new BigDecimal(100)).longValue();
        }
        // 总价格 = 物品价格 + 保价费用
        long totalPrice = itemPriceTotal + insuranceFeeFen;
        PriceCalculateVO result = new PriceCalculateVO();
        result.setItemList(itemList);
        result.setItemPrice(itemPriceTotal);
        result.setInsuranceFee(insuranceFeeFen);
        result.setTotalPrice(totalPrice);
        result.setDays(days);
        result.setUrgentFee(0L);
        return result;
    }
    /**
     * 计算异地存取预估费用
     *
     * 计算规则:
     * 1. 调用腾讯地图API计算寄件点与取件点的驾车距离(米→公里)
     * 2. 根据城市+物品类型 查询 pricing_rule(type=1):
     *    fieldB=起步距离(km), fieldC=起步价(分), fieldD=超出距离单位(km), fieldE=超出距离单价(分)
     * 3. 每项运费单价:
     *    - 距离 ≤ 起步距离 → 单价 = 起步价
     *    - 距离 > 起步距离 → 单价 = 起步价 + ceil((距离-起步距离)/超出距离单位) × 超出距离单价
     * 4. 小计 = 运费单价 × 数量
     * 5. 物品价格 = 各项小计之和
     * 6. 保价费用 = 报价金额 × 保价费率(字典 INSURANCE_RATE),元转分
     * 7. 加急费用 = 物品价格 × 加急系数(字典 URGENT_COEFFICIENT)
     * 8. 总价格 = 物品价格 + 保价费用 + 加急费用
     *
     * @param dto 异地存取计价请求参数
     * @return 价格计算结果
     */
    @Override
    public PriceCalculateVO calculateRemotePrice(CalculateRemotePriceDTO dto) {
        // 1. 调用腾讯地图距离矩阵API计算驾车距离
        String from = dto.getFromLat() + "," + dto.getFromLgt();
        String to = dto.getToLat() + "," + dto.getToLgt();
        JSONObject distanceResult = MapUtil.distanceSingle("driving", from, to);
        BigDecimal distance = distanceResult.getBigDecimal("distance");
        // distance 单位为米,转为公里
        BigDecimal distanceKm = distance.divide(new BigDecimal(1000), 2, RoundingMode.HALF_UP);
        // 收集所有物品类型ID
        List<Integer> categoryIds = new ArrayList<>();
        for (OrderItemDTO item : dto.getItems()) {
            categoryIds.add(item.getCategoryId());
        }
        // 2. 批量查询配送计价规则 pricing_rule type=1
        List<String> fieldAList = new ArrayList<>();
        for (Integer cid : categoryIds) {
            fieldAList.add(String.valueOf(cid));
        }
        List<PricingRule> rules = pricingRuleMapper.selectList(new QueryWrapper<PricingRule>().lambda()
                .eq(PricingRule::getDeleted, Constants.ZERO)
                .eq(PricingRule::getType, Constants.ONE)
                .eq(PricingRule::getCityId, dto.getCityId())
                .in(PricingRule::getFieldA, fieldAList));
        Map<String, PricingRule> ruleMap = new HashMap<>();
        for (PricingRule r : rules) {
            ruleMap.put(r.getFieldA(), r);
        }
        // 查询就地存取计价规则 pricing_rule type=0,用于获取 locallyPrice
        List<PricingRule> localRules = pricingRuleMapper.selectList(new QueryWrapper<PricingRule>().lambda()
                .eq(PricingRule::getDeleted, Constants.ZERO)
                .eq(PricingRule::getType, Constants.ZERO)
                .eq(PricingRule::getCityId, dto.getCityId())
                .in(PricingRule::getFieldA, fieldAList));
        Map<String, PricingRule> localRuleMap = new HashMap<>();
        for (PricingRule r : localRules) {
            localRuleMap.put(r.getFieldA(), r);
        }
        // 批量查询物品类型名称
        List<Category> categories = categoryMapper.selectBatchIds(categoryIds);
        Map<Integer, String> categoryNameMap = new HashMap<>();
        Map<Integer, String> categoryDetailMap = new HashMap<>();
        for (Category c : categories) {
            categoryNameMap.put(c.getId(), c.getName());
            categoryDetailMap.put(c.getId(), c.getDetail());
        }
        // 3. 逐项计算运费:起步价 + 超出部分阶梯价
        List<ItemPriceVO> itemList = new ArrayList<>();
        long itemPriceTotal = 0L;
        for (OrderItemDTO item : dto.getItems()) {
            PricingRule rule = ruleMap.get(String.valueOf(item.getCategoryId()));
            if (rule == null) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(),
                        "未找到该城市物品类型的配送计价规则");
            }
            // fieldB=起步距离(km), fieldC=起步价(分), fieldD=超出距离单位(km), fieldE=超出距离单价(分)
            BigDecimal startDistance = new BigDecimal(rule.getFieldB());
            long startPrice = Long.parseLong(rule.getFieldC());
            BigDecimal extraDistanceUnit = new BigDecimal(rule.getFieldD());
            long extraPricePerUnit = Long.parseLong(rule.getFieldE());
            // 阶梯计价:距离 ≤ 起步距离取起步价,超出按 ceil(超出距离/单位) × 单价累加
            long unitPrice;
            if (distanceKm.compareTo(startDistance) <= 0) {
                unitPrice = startPrice;
            } else {
                BigDecimal extraKm = distanceKm.subtract(startDistance);
                BigDecimal extraCount = extraKm.divide(extraDistanceUnit, 0, RoundingMode.CEILING);
                unitPrice = startPrice + extraCount.longValue() * extraPricePerUnit;
            }
            long subtotal = unitPrice * item.getQuantity();
            // 就地存取单价
            PricingRule localRule = localRuleMap.get(String.valueOf(item.getCategoryId()));
            Long locallyPrice = localRule != null ? Long.parseLong(localRule.getFieldB()) : null;
            ItemPriceVO vo = new ItemPriceVO();
            vo.setCategoryId(item.getCategoryId());
            vo.setCategoryName(categoryNameMap.getOrDefault(item.getCategoryId(), ""));
            vo.setDetail(categoryDetailMap.get(item.getCategoryId()));
            vo.setQuantity(item.getQuantity());
            vo.setUnitPrice(unitPrice);
            vo.setLocallyPrice(locallyPrice);
            vo.setSubtotal(subtotal);
            vo.setStartDistance(startDistance);
            vo.setStartPrice(startPrice);
            vo.setExtraDistance(extraDistanceUnit);
            vo.setExtraPrice(extraPricePerUnit);
            itemList.add(vo);
            itemPriceTotal += subtotal;
        }
        // 4. 保价费用:报价金额 × 保价费率(字典 INSURANCE_RATE),元→分
        long insuranceFeeFen = 0L;
        if (Boolean.TRUE.equals(dto.getInsured()) && dto.getDeclaredAmount() != null) {
            BigDecimal insuranceFeeYuan = calculateInsuranceFee(dto.getDeclaredAmount());
            insuranceFeeFen = insuranceFeeYuan.multiply(new BigDecimal(100)).longValue();
        }
        // 5. 加急费用:物品价格 × 加急系数(字典 URGENT_COEFFICIENT)
        long urgentFeeFen = 0L;
        if (Boolean.TRUE.equals(dto.getUrgent())) {
            String urgentRateStr = systemDictDataBiz.queryByCode(
                    Constants.OPERATION_CONFIG, Constants.OP_URGENT_COEFFICIENT).getCode();
            BigDecimal urgentRate = new BigDecimal(urgentRateStr);
            urgentFeeFen = new BigDecimal(itemPriceTotal).multiply(urgentRate)
                    .setScale(0, RoundingMode.HALF_UP).longValue();
        }
        // 6. 总价格 = 物品价格 + 保价费用 + 加急费用
        long totalPrice = itemPriceTotal + insuranceFeeFen + urgentFeeFen;
        PriceCalculateVO result = new PriceCalculateVO();
        result.setItemList(itemList);
        result.setItemPrice(itemPriceTotal);
        result.setInsuranceFee(insuranceFeeFen);
        result.setUrgentFee(urgentFeeFen);
        result.setTotalPrice(totalPrice);
        result.setDistance(distanceKm);
        return result;
    }
    @Override
    public long count(Orders orders) {
        QueryWrapper<Orders> wrapper = new QueryWrapper<>(orders);
        return ordersMapper.selectCount(wrapper);
    }
    /**
     * 创建订单
     *
     * 业务流程:
     * 1. 参数校验:必填字段、时间顺序、物品不重复、异地必填服务时效
     * 2. 查询寄件店铺信息(获取经纬度、地址)
     * 3. 调用计价接口计算费用(calculateLocalPrice / calculateRemotePrice)
     * 4. 生成订单编号:JC + yyyyMMddHHmmss + 4位随机数
     * 5. 创建订单主表 Orders(状态=待支付)
     * 6. 创建订单明细表 OrdersDetail(每项物品尺寸)
     * 7. 创建附件记录 Multifile(物品图片 objType=12)
     *
     * @param dto      创建订单请求参数
     * @param memberId 当前登录会员ID
     * @return 订单ID
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public PayResponse createOrder(CreateOrderDTO dto, Integer memberId) {
        String lockKey  = Constants.GOODS_ORDER_CREATE_LOCK + memberId;
        //判断前端是否在同一页面创建了两次订单
        if (redisTemplate.hasKey(lockKey)) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"正在创建订单,请勿重复调用!");
        } else {
            redisTemplate.opsForValue().set(lockKey, "", 5, TimeUnit.SECONDS);
        }
        Date now = new Date();
        // ========== 1. 参数校验 ==========
        // 预计到店存件时间必须小于预计到店取件时间
        java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm");
        Date depositTime;
        Date takeTime;
        try {
            depositTime = sdf.parse(dto.getExpectedDepositTime());
            takeTime = sdf.parse(dto.getExpectedTakeTime());
        } catch (java.text.ParseException e) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "时间格式错误,正确格式:yyyy-MM-dd HH:mm");
        }
        if (!depositTime.before(takeTime)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "预计到店存件时间必须小于预计到店取件时间");
        }
        // 物品尺寸不能重复
        List<Integer> categoryIds = new ArrayList<>();
        for (OrderItemDTO item : dto.getItems()) {
            if (categoryIds.contains(item.getCategoryId())) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "物品尺寸不能重复");
            }
            categoryIds.add(item.getCategoryId());
        }
        // 物品图片最多3张
        if (dto.getGoodsImages() != null && dto.getGoodsImages().size() > 3) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "物品图片最多3张");
        }
        // ========== 2. 校验物品类型 ==========
        Category goodTypeCategory = categoryMapper.selectById(dto.getGoodType());
        if (goodTypeCategory == null || Constants.equalsInteger(goodTypeCategory.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "物品类型不存在");
        }
        if (!Constants.equalsInteger(goodTypeCategory.getType(), Constants.TWO)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "物品类型参数错误");
        }
        // ========== 3. 查询寄件店铺信息 ==========
        ShopInfo depositShop = shopInfoMapper.selectById(dto.getDepositShopId());
        if (depositShop == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "寄件店铺不存在");
        }
        // ========== 4. 计算费用 ==========
        // 异地寄存:校验取件点
        BigDecimal takeLat = null;
        BigDecimal takeLgt = null;
        String takeLocationValue = null;
        ShopInfo takeShop = null;
        if (Constants.ONE.equals(dto.getType())) {
            // 异地必填服务时效
            if (dto.getIsUrgent() == null) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "异地寄存服务时效不能为空");
            }
            // 取件点:店铺 or 自选点,至少提供一组
            if (dto.getTakeShopId() != null) {
                takeShop = shopInfoMapper.selectById(dto.getTakeShopId());
                if (takeShop == null) {
                    throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "取件店铺不存在");
                }
                takeLat = BigDecimal.valueOf(takeShop.getLatitude());
                takeLgt = BigDecimal.valueOf(takeShop.getLongitude());
                takeLocationValue = takeShop.getAddress();
            } else if (dto.getTakeLat() != null && dto.getTakeLgt() != null && StringUtils.isNotBlank(dto.getTakeLocation())) {
                takeLat = dto.getTakeLat();
                takeLgt = dto.getTakeLgt();
                takeLocationValue = dto.getTakeLocation();
            } else {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "请选择取件店铺或输入自选取件地址");
            }
        } else {
            // 就地存取:取件门店同寄件门店
            takeShop = depositShop;
        }
        // ========== 3. 计算费用 ==========
        PriceCalculateVO priceResult;
        if (Constants.ZERO.equals(dto.getType())) {
            // 就地寄存:计算天数
            long diffMs = takeTime.getTime() - depositTime.getTime();
            int days = (int) Math.max(1, (diffMs / (1000 * 60 * 60 * 24)) + 1);
            CalculateLocalPriceDTO priceDTO = new CalculateLocalPriceDTO();
            priceDTO.setCityId(dto.getCityId());
            priceDTO.setEstimatedDepositDays(days);
            priceDTO.setItems(dto.getItems());
            priceDTO.setInsured(dto.getDeclaredAmount() != null && dto.getDeclaredAmount().compareTo(BigDecimal.ZERO) > 0);
            priceDTO.setDeclaredAmount(dto.getDeclaredAmount());
            priceResult = calculateLocalPrice(priceDTO);
        } else {
            // 异地寄存
            CalculateRemotePriceDTO priceDTO = new CalculateRemotePriceDTO();
            priceDTO.setCityId(dto.getCityId());
            priceDTO.setFromLat(BigDecimal.valueOf(depositShop.getLatitude()));
            priceDTO.setFromLgt(BigDecimal.valueOf(depositShop.getLongitude()));
            priceDTO.setToLat(takeLat);
            priceDTO.setToLgt(takeLgt);
            priceDTO.setItems(dto.getItems());
            priceDTO.setInsured(dto.getDeclaredAmount() != null && dto.getDeclaredAmount().compareTo(BigDecimal.ZERO) > 0);
            priceDTO.setDeclaredAmount(dto.getDeclaredAmount());
            priceDTO.setUrgent(Constants.ONE.equals(dto.getIsUrgent()));
            priceResult = calculateRemotePrice(priceDTO);
        }
        // ========== 5. 生成订单编号 ==========
        String orderCode = "JC" + new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(now)
                + String.format("%04d", new java.util.Random().nextInt(10000));
        // 生成32位唯一第三方订单编号
        String orderTradeNo = generateOrderTradeNo();
        // ========== 6. 创建订单主表 ==========
        Orders orders = new Orders();
        orders.setCode(orderCode);
        orders.setOutTradeNo(orderTradeNo);
        orders.setMemberId(memberId);
        orders.setType(dto.getType());
        orders.setCityId(String.valueOf(dto.getCityId()));
        orders.setStatus(Constants.ZERO); // 待支付
        orders.setPayStatus(Constants.ZERO); // 未支付
        orders.setCommentStatus(Constants.ZERO); // 未评价
        orders.setSettlementStatus(Constants.ZERO); // 未结算
        orders.setDeleted(Constants.ZERO);
        orders.setCreateTime(now);
        orders.setUpdateTime(now);
        // 寄件信息
        orders.setDepositShopId(dto.getDepositShopId());
        // 存件地点:省市区全路径 + 地址描述
        String depositLocationRemark = depositShop.getAddress();
        if (depositShop.getAreaId() != null) {
            Areas depositArea = areasBiz.resolveArea(depositShop.getAreaId());
            if (depositArea != null) {
                depositLocationRemark = depositArea.getProvinceName() + depositArea.getCityName() + depositArea.getName() + depositShop.getAddress();
            }
        }
        orders.setDepositLocation(depositLocationRemark);
        orders.setDepositLocationRemark(depositShop.getAddress());
        orders.setDepositLat(BigDecimal.valueOf(depositShop.getLatitude()));
        orders.setDepositLgt(BigDecimal.valueOf(depositShop.getLongitude()));
        // 取件信息
        orders.setTakeUser(dto.getTakeUser());
        orders.setTakePhone(dto.getTakePhone());
        orders.setExpectedDepositTime(depositTime);
        orders.setExpectedTakeTime(takeTime);
        // 计算预计存放天数
        long dayDiff = (takeTime.getTime() - depositTime.getTime()) / (1000 * 60 * 60 * 24);
        orders.setEstimatedDepositDays((int) Math.max(1, dayDiff + 1));
        if (Constants.ONE.equals(dto.getType())) {
            // 异地:取件点信息
            orders.setTakeShopId(dto.getTakeShopId());
            orders.setTakeLocation(takeLocationValue);
            orders.setTakeLat(takeLat);
            orders.setTakeLgt(takeLgt);
            orders.setIsUrgent(dto.getIsUrgent());
        } else {
            // 就地:取件点同寄件店铺
            orders.setTakeShopId(dto.getDepositShopId());
            orders.setTakeLocation(depositShop.getAddress());
            orders.setTakeLat(BigDecimal.valueOf(depositShop.getLatitude()));
            orders.setTakeLgt(BigDecimal.valueOf(depositShop.getLongitude()));
            orders.setIsUrgent(Constants.ZERO);
        }
        // 物品信息
        orders.setGoodType(dto.getGoodType());
        // 拼接物品信息:物品类型名称、尺寸名称*数量(数组字符串)
        List<String> goodsParts = new ArrayList<>();
        for (ItemPriceVO itemVO : priceResult.getItemList()) {
            goodsParts.add(itemVO.getCategoryName() + "*" + itemVO.getQuantity());
        }
        orders.setGoodsInfo(goodTypeCategory.getName() + "、" + String.join(",", goodsParts));
        orders.setRemark(dto.getRemark());
        orders.setSelfTake(Constants.ZERO);
        // 费用信息(分)
        orders.setBasicAmount(priceResult.getItemPrice());
        orders.setEstimatedAmount(priceResult.getTotalPrice());
        orders.setTotalAmount(priceResult.getTotalPrice());
        orders.setUrgentAmount(priceResult.getUrgentFee());
        if (dto.getDeclaredAmount() != null && dto.getDeclaredAmount().compareTo(BigDecimal.ZERO) > 0) {
            orders.setDeclaredAmount(dto.getDeclaredAmount().multiply(new BigDecimal(100)).longValue());
        } else {
            orders.setDeclaredAmount(0L);
        }
        orders.setDeclaredFee(priceResult.getInsuranceFee());
        orders.setPrice(priceResult.getItemPrice());
        // 薪酬计算与占比存储
        calculateAndSetFeeAllocation(orders, depositShop, takeShop);
        ordersMapper.insert(orders);
        Integer orderId = orders.getId();
        // ========== 7. 创建订单明细 ==========
        for (ItemPriceVO itemVO : priceResult.getItemList()) {
            OrdersDetail detail = new OrdersDetail();
            detail.setOrderId(orderId);
            detail.setLuggageId(itemVO.getCategoryId());
            detail.setLuggageName(itemVO.getCategoryName());
            detail.setLuggageDetail(itemVO.getDetail());
            detail.setNum(itemVO.getQuantity());
            detail.setUnitPrice(itemVO.getUnitPrice());
            detail.setStartDistance(itemVO.getStartDistance());
            detail.setStartPrice(itemVO.getStartPrice());
            detail.setExtraDistance(itemVO.getExtraDistance());
            detail.setExtraPrice(itemVO.getExtraPrice());
            detail.setLocallyPrice(itemVO.getLocallyPrice());
            detail.setDeleted(Constants.ZERO);
            detail.setCreateTime(now);
            ordersDetailMapper.insert(detail);
        }
        // ========== 8. 保存物品图片附件 ==========
        if (dto.getGoodsImages() != null && !dto.getGoodsImages().isEmpty()) {
            int sortNum = 1;
            for (String imgUrl : dto.getGoodsImages()) {
                Multifile multifile = new Multifile();
                multifile.setObjId(orderId);
                multifile.setObjType(Constants.FileType.ORDER_FILE.getKey());
                multifile.setType(Constants.ZERO);
                multifile.setFileurl(imgUrl);
                multifile.setIsdeleted(Constants.ZERO);
                multifile.setCreateDate(now);
                multifile.setSortnum(sortNum++);
                multifileMapper.insert(multifile);
            }
        }
        // ========== 9. 唤起微信支付 ==========
        Member member = memberMapper.selectById(memberId);
        if (member == null || StringUtils.isBlank(member.getOpenid())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "用户信息异常,无法发起支付");
        }
        PayResponse payResponse = wxPay(orders, member.getOpenid(), Constants.OrdersAttach.STORAGE_ORDER);
        payResponse.setLockKey(lockKey);
        return payResponse;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public PayResponse continuePay(Integer orderId, Integer memberId) {
        // 1. 查询订单
        Orders orders = ordersMapper.selectById(orderId);
        if (Objects.isNull(orders) || Constants.equalsInteger(orders.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        // 2. 校验订单归属
        if (!Constants.equalsInteger(orders.getMemberId(), memberId)) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "无权操作该订单");
        }
        // 3. 校验订单状态:仅待支付可继续支付
        if (!Constants.equalsInteger(orders.getStatus(), Constants.ZERO)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不支持继续支付");
        }
        // 4. 校验支付金额
        if (orders.getTotalAmount() == null || orders.getTotalAmount() <= 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "订单金额异常,无法发起支付");
        }
        // 5. 重新生成第三方订单编号(避免重复)
        String orderTradeNo = generateOrderTradeNo();
        orders.setOutTradeNo(orderTradeNo);
        orders.setUpdateTime(new Date());
        ordersMapper.updateById(orders);
        // 6. 唤起微信支付
        Member member = memberMapper.selectById(memberId);
        if (member == null || StringUtils.isBlank(member.getOpenid())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "用户信息异常,无法发起支付");
        }
        return wxPay(orders, member.getOpenid(), Constants.OrdersAttach.STORAGE_ORDER);
    }
    /**
     * 唤起微信小程序支付
     *
     * @param orders       订单实体(需要 code、totalAmount)
     * @param openid       用户微信openid
     * @param ordersAttach 订单支付类型
     * @return PayResponse 包含微信调起参数和订单主键
     */
    private PayResponse wxPay(Orders orders, String openid, Constants.OrdersAttach ordersAttach) {
        try {
            WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest();
            request.setBody(ordersAttach.getName());
            request.setAttach(ordersAttach.getKey());
            request.setOutTradeNo(orders.getOutTradeNo());
            // totalAmount 单位为分,WeChat Pay setTotalFee 也是分,直接转int
            long totalFee = orders.getTotalAmount() != null ? orders.getTotalAmount() : 0L;
            request.setTotalFee((int) totalFee);
            request.setTimeStart(DateUtil.DateToString(new Date(), "yyyyMMddHHmmss"));
            request.setSpbillCreateIp(Constants.getIpAddr());
            request.setOpenid(openid);
            Object response = WxMiniConfig.wxPayService.createOrder(request);
            PayResponse payResponse = new PayResponse();
            payResponse.setResponse(response);
            payResponse.setOrderId(orders.getId());
            return payResponse;
        } catch (WxPayException e) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "支付调起失败:" + e.getMessage());
        }
    }
    @Override
    public OrderDetailVO findDetail(Integer id) {
        Orders order = ordersMapper.selectById(id);
        if (Objects.isNull(order)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        OrderDetailVO vo = new OrderDetailVO();
        vo.setOrder(order);
        // 图片路径前缀
        String imgPrefix = getOrdersPrefix();
        // 下单图片 (type=12)
        vo.setOrderFiles(getFileUrls(id, Constants.FileType.ORDER_FILE.getKey(), imgPrefix));
        // 会员信息
        if (order.getMemberId() != null) {
            Member member = memberMapper.selectById(order.getMemberId());
            if (member != null) {
                vo.setMemberName(member.getName());
                vo.setMemberPhone(member.getTelephone());
            }
        }
        // 寄存门店信息
        if (order.getDepositShopId() != null) {
            ShopInfo depositShop = shopInfoMapper.selectById(order.getDepositShopId());
            if (depositShop != null) {
                vo.setDepositShopName(depositShop.getName());
                vo.setDepositShopPhone(depositShop.getLinkPhone());
            }
        }
        // 取件门店信息
        if (order.getTakeShopId() != null) {
            ShopInfo takeShop = shopInfoMapper.selectById(order.getTakeShopId());
            if (takeShop != null) {
                vo.setTakeShopName(takeShop.getName());
                vo.setTakeShopAddress(takeShop.getAddress());
                vo.setTakeShopPhone(takeShop.getLinkPhone());
            }
        }
        // 接单司机信息
        if (order.getAcceptDriver() != null) {
            DriverInfo driverInfo = driverInfoMapper.selectById(order.getAcceptDriver());
            if (driverInfo != null) {
                vo.setDriverName(driverInfo.getName());
            }
        }
        // 配送附件图片
        vo.setDepositImages(getFileUrls(id, Constants.FileType.ORDER_DEPOSIT.getKey(), imgPrefix));
        vo.setStoreInImages(getFileUrls(id, Constants.FileType.ORDER_TAKE.getKey(), imgPrefix));
        vo.setDriverTakeImages(getFileUrls(id, Constants.FileType.DRIVER_TAKE.getKey(), imgPrefix));
        vo.setDriverDoneImages(getFileUrls(id, Constants.FileType.DRIVER_DONE.getKey(), imgPrefix));
        vo.setStoreOutImages(getFileUrls(id, Constants.FileType.STORE_OUT.getKey(), imgPrefix));
        // 物品明细
        vo.setDetailList(buildDetailList(id));
        Category category = categoryMapper.selectById(order.getGoodType());
        if(CollectionUtils.isNotEmpty(vo.getDetailList())&&Objects.nonNull(category)){
            for (OrderItemVO v:vo.getDetailList()) {
                v.setTypeName(category.getName());
            }
        }
        // 取消/退款状态时查询退款记录
        Integer status = order.getStatus();
        if (status != null && (status == Constants.OrderStatus.overdue.getStatus()
                || status == Constants.OrderStatus.closed.getStatus()
                || status == Constants.OrderStatus.cancelOverdue.getStatus()
                || status == Constants.OrderStatus.cancelling.getStatus()
                || status == Constants.OrderStatus.cancelled.getStatus())) {
            OrdersRefund ordersRefund = ordersRefundMapper.selectOne(
                    new QueryWrapper<OrdersRefund>().lambda()
                            .eq(OrdersRefund::getOrderId, id)
                            .eq(OrdersRefund::getDeleted, Constants.ZERO)
                            .orderByDesc(OrdersRefund::getCreateTime)
                            .last("limit 1"));
            if (ordersRefund != null) {
                vo.setOrdersRefund(ordersRefund);
                // 退款方式:1=平台直接取消 → 返回平台操作人名称
                if (Constants.equalsInteger(ordersRefund.getType(), Constants.ONE) && ordersRefund.getUserId() != null) {
                    // userId 关联 system_user 表,查询操作人名称
                    vo.setPlatformUserName(getPlatformUserName(ordersRefund.getUserId()));
                }
                // 退款方式:2=已存件申请取消 → 返回退款取件图片 (multifile objType=14)
                if (Constants.equalsInteger(ordersRefund.getType(), Constants.TWO)) {
                    vo.setRefundTakeImages(getFileUrls(id, Constants.FileType.REFUND_TAKE.getKey(), imgPrefix));
                }
            }
        }
        return vo;
    }
    @Override
    public OrderDispatchVO findDispatchInfo(Integer id) {
        Orders order = ordersMapper.selectById(id);
        if (Objects.isNull(order)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        OrderDispatchVO vo = new OrderDispatchVO();
        vo.setCode(order.getCode());
        vo.setPayAmountYuan(order.getPayAmount() != null ? Constants.getFormatMoney(order.getPayAmount()) : 0);
        vo.setType(order.getType());
        vo.setTypeDesc(order.getType() != null && order.getType() == Constants.ONE ? "异地存取" : "就地存取");
        vo.setDetailList(buildDetailList(id));
        return vo;
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void dispatch(DispatchDTO dto) {
        // 参数校验
        if (dto == null || dto.getOrderId() == null || dto.getUrgentFee() == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "订单主键和加急费用不能为空");
        }
        Orders order = ordersMapper.selectById(dto.getOrderId());
        if (Objects.isNull(order)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        // 前置条件校验:异地存取 + 已寄存
        if (!Constants.ONE.equals(order.getType())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅支持异地存取订单派单");
        }
        if (!Integer.valueOf(Constants.OrderStatus.deposited.getStatus()).equals(order.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅已寄存状态订单可派单");
        }
        String optUserName = getCurrentUserName();
        // 加急费日志(每次单独记录本次加急费)
        Constants.OrderLogType urgentLogType = Constants.OrderLogType.urgent;
        OrderLog feeLog = new OrderLog();
        feeLog.setOrderId(order.getId());
        feeLog.setTitle(urgentLogType.getTitle());
        feeLog.setLogInfo(urgentLogType.getStatusInfo().replace("{param}", dto.getUrgentFee().toPlainString()));
        feeLog.setObjType(urgentLogType.getStatus());
        feeLog.setOrderStatus(order.getStatus());
        feeLog.setOptUserType(3);
        feeLog.setOptUserName(optUserName);
        feeLog.setCreateTime(new Date());
        feeLog.setDeleted(Constants.ZERO);
        orderLogService.create(feeLog);
        // 加急费用 元→分
        long urgentFeeFen = dto.getUrgentFee().multiply(new BigDecimal(100)).longValue();
        // 使用 UpdateWrapper 精确更新,不影响其他字段
        com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<Orders> updateWrapper =
                new UpdateWrapper<Orders>().lambda()
                .eq(Orders::getId, order.getId())
                .set(Orders::getIsUrgent, Constants.ONE)
                .set(Orders::getPlatformRewardAmount, urgentFeeFen)
                .set(Orders::getUpdateTime, new Date());
        // 异地寄存且有取件门店时,生成司机核销码
        if (order.getTakeShopId() != null) {
            String driverVerifyCode = generateVerifyCode();
            updateWrapper.set(Orders::getDriverVerifyCode, driverVerifyCode);
        }
        // 备注
        if (StringUtils.isNotBlank(dto.getRemark())) {
            updateWrapper.set(Orders::getRemark, dto.getRemark());
        }
        // 指派司机(非必填)
        if (dto.getDriverId() != null) {
            updateWrapper.set(Orders::getAssignDriverId, dto.getDriverId());
            Member driver = memberMapper.selectById(dto.getDriverId());
            String driverName = driver != null ? driver.getName() : String.valueOf(dto.getDriverId());
            Constants.OrderLogType dispatchLogType = Constants.OrderLogType.dispatch;
            OrderLog driverLog = new OrderLog();
            driverLog.setOrderId(order.getId());
            driverLog.setTitle(dispatchLogType.getTitle());
            driverLog.setLogInfo(dispatchLogType.getStatusInfo().replace("{param}", driverName));
            driverLog.setObjType(dispatchLogType.getStatus());
            driverLog.setOrderStatus(order.getStatus());
            driverLog.setOptUserType(3);
            driverLog.setOptUserName(optUserName);
            driverLog.setCreateTime(new Date());
            driverLog.setDeleted(Constants.ZERO);
            orderLogService.create(driverLog);
        }
        ordersMapper.update(updateWrapper);
    }
    private String getCurrentUserName() {
        try {
            com.doumee.core.model.LoginUserInfo user =
                    (com.doumee.core.model.LoginUserInfo) org.apache.shiro.SecurityUtils.getSubject().getPrincipal();
            return user != null ? user.getUsername() : "系统";
        } catch (Exception e) {
            return "系统";
        }
    }
    /**
     * 构建订单物品明细列表
     */
    private List<OrderItemVO> buildDetailList(Integer orderId) {
        List<OrdersDetail> details = ordersDetailMapper.selectList(
                new QueryWrapper<OrdersDetail>().lambda()
                        .eq(OrdersDetail::getOrderId, orderId)
                        .eq(OrdersDetail::getDeleted, Constants.ZERO));
        return buildDetailList(details);
    }
    /**
     * 根据已查询的明细构建物品列表(避免重复查询)
     */
    private List<OrderItemVO> buildDetailList(List<OrdersDetail> details) {
        List<OrderItemVO> items = new ArrayList<>();
        if (details != null) {
            for (OrdersDetail d : details) {
                OrderItemVO item = new OrderItemVO();
                item.setLuggageName(d.getLuggageName());
                item.setLuggageDetail(d.getLuggageDetail());
                item.setNum(d.getNum());
                double unitPriceYuan = d.getUnitPrice() != null ? Constants.getFormatMoney(d.getUnitPrice()) : 0;
                item.setUnitPriceYuan(unitPriceYuan);
                item.setSubtotal(unitPriceYuan * (d.getNum() != null ? d.getNum() : 0));
                items.add(item);
            }
        }
        return items;
    }
    /**
     * 生成32位唯一第三方订单编号(时间戳17位 + 随机数15位)
     */
    private String generateOrderTradeNo() {
        return new java.text.SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date())
                + String.format("%015d", Math.abs(new java.util.Random().nextLong() % 1000000000000000L));
    }
    /**
     * 生成6位数字核销码(Redis验重)
     * 使用 SETNX 抢占,保证唯一;使用完毕后调用 releaseVerifyCode 从 Redis 移除
     */
    private String generateVerifyCode() {
        Random random = new Random();
        String redisKey = Constants.REDIS_VERIFY_CODE_KEY;
        for (int i = 0; i < 200; i++) {
            String code = String.format("%06d", random.nextInt(1000000));
            Boolean success = redisTemplate.opsForValue().setIfAbsent(redisKey + code, "1", 24, TimeUnit.HOURS);
            if (success != null && success) {
                return code;
            }
        }
        throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "核销码生成失败,请重试");
    }
    /**
     * 释放核销码占位(核销完成后调用,移除 Redis 中的 key)
     */
    public void releaseVerifyCode(String code) {
        if (StringUtils.isNotBlank(code)) {
            redisTemplate.delete(Constants.REDIS_VERIFY_CODE_KEY + code);
        }
    }
    private String getOrdersPrefix() {
        try {
            return systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.RESOURCE_PATH).getCode()
                    + systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.ORDERS_FILES).getCode();
        } catch (Exception e) {
            return "";
        }
    }
    private String getPlatformUserName(Integer userId) {
        SystemUser user = systemUserMapper.selectById(userId);
        return user != null ? user.getRealname() : null;
    }
    private List<String> getFileUrls(Integer orderId, int objType, String prefix) {
        List<Multifile> files = multifileMapper.selectList(
                new QueryWrapper<Multifile>().lambda()
                        .eq(Multifile::getObjId, orderId)
                        .eq(Multifile::getObjType, objType)
                        .eq(Multifile::getIsdeleted, Constants.ZERO)
                        .orderByAsc(Multifile::getSortnum));
        List<String> urls = new ArrayList<>();
        if (files != null) {
            for (Multifile f : files) {
                if (StringUtils.isNotBlank(f.getFileurl())) {
                    urls.add(prefix + f.getFileurl());
                }
            }
        }
        return urls;
    }
    /**
     * 计算并设置订单薪酬分配(司机、存件门店、取件门店)
     * 从 pricing_rule (type=4) 读取分成比例,根据门店企业/个人类型区分
     *
     * @param orders        订单实体(需要 totalAmount、cityId 已设置)
     * @param depositShop   寄件门店(需要 companyType)
     * @param takeShop      取件门店(需要 companyType,就地存取时与 depositShop 相同)
     */
    private void calculateAndSetFeeAllocation(Orders orders, ShopInfo depositShop, ShopInfo takeShop) {
        Long totalAmount = orders.getTotalAmount() != null ? orders.getTotalAmount() : 0L;
        if (totalAmount <= 0) {
            orders.setDriverFee(0L);
            orders.setDepositShopFee(0L);
            orders.setTakeShopFee(0L);
            orders.setDriverFeeRata(BigDecimal.ZERO);
            orders.setDepositShopFeeRata(BigDecimal.ZERO);
            orders.setTakeShopFeeRata(BigDecimal.ZERO);
            return;
        }
        Integer cityId = Integer.valueOf(orders.getCityId());
        // 司机占比:fieldA=4(配送员)
        BigDecimal driverRata = getRevenueShareRata(cityId, Constants.FOUR);
        // 寄件门店占比:fieldA=0(企业寄)/1(个人寄)
        int depositFieldA = Constants.equalsInteger(depositShop.getCompanyType(), Constants.ONE) ? Constants.ZERO : Constants.ONE;
        BigDecimal depositShopRata = getRevenueShareRata(cityId, depositFieldA);
        // 取件门店占比:fieldA=2(企业取)/3(个人取)
        int takeFieldA = Constants.equalsInteger(takeShop.getCompanyType(), Constants.ONE) ? Constants.TWO : Constants.THREE;
        BigDecimal takeShopRata = getRevenueShareRata(cityId, takeFieldA);
        // 计算薪酬(分):totalAmount 为分,rata 为比例值(如 0.15 表示 15%)
        long driverFee = new BigDecimal(totalAmount).multiply(driverRata).longValue();
        long depositShopFee = new BigDecimal(totalAmount).multiply(depositShopRata).longValue();
        long takeShopFee = totalAmount - driverFee - depositShopFee;
        orders.setDriverFee(driverFee);
        orders.setDepositShopFee(depositShopFee);
        orders.setTakeShopFee(takeShopFee);
        orders.setDriverFeeRata(driverRata);
        orders.setDepositShopFeeRata(depositShopRata);
        orders.setTakeShopFeeRata(takeShopRata);
    }
    /**
     * 从 pricing_rule 表获取分成比例(type=4)
     *
     * @param cityId   城市主键
     * @param fieldA   类型:0=企业寄, 1=个人寄, 2=企业取, 3=个人取, 4=配送员
     * @return 分成比例(如 0.15 表示 15%)
     */
    private BigDecimal getRevenueShareRata(Integer cityId, int fieldA) {
        PricingRule rule = pricingRuleMapper.selectOne(new QueryWrapper<PricingRule>().lambda()
                .eq(PricingRule::getDeleted, Constants.ZERO)
                .eq(PricingRule::getType, Constants.FOUR)
                .eq(PricingRule::getCityId, cityId)
                .eq(PricingRule::getFieldA, String.valueOf(fieldA))
                .last("limit 1"));
        if (rule != null && StringUtils.isNotBlank(rule.getFieldC())) {
            return new BigDecimal(rule.getFieldC());
        }
        return BigDecimal.ZERO;
    }
    @Override
    public PageData<MyOrderVO> findMyOrderPage(PageWrap<MyOrderDTO> pageWrap, Integer memberId) {
        MyOrderDTO model = pageWrap.getModel();
        Integer status = model != null ? model.getStatus() : null;
        Integer combinedStatus = model != null ? model.getCombinedStatus() : null;
        // 解析合并状态为具体状态列表
        List<Integer> statusList = null;
        if (combinedStatus != null) {
            Constants.OrderCombinedStatus combined = Constants.OrderCombinedStatus.getByKey(combinedStatus);
            if (combined != null) {
                statusList = new ArrayList<>();
                for (int s : combined.getStatuses()) {
                    statusList.add(s);
                }
            }
        }
        IPage<Orders> p = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        MPJLambdaWrapper<Orders> wrapper = new MPJLambdaWrapper<Orders>()
                .selectAll(Orders.class)
                .select("s1.name", Orders::getDepositShopName)
                .select("s1.link_name", Orders::getDepositShopLinkName)
                .select("s1.link_phone", Orders::getDepositShopLinkPhone)
                .select("s2.name", Orders::getTakeShopName)
                .select("s2.address", Orders::getTakeShopAddress)
                .leftJoin("shop_info s1 on s1.id = t.DEPOSIT_SHOP_ID")
                .leftJoin("shop_info s2 on s2.id = t.TAKE_SHOP_ID")
                .eq(Orders::getDeleted, Constants.ZERO)
                .eq(Orders::getMemberId, memberId)
                .eq(status != null, Orders::getStatus, status)
                .in(statusList != null, Orders::getStatus, statusList)
                .orderByDesc(Orders::getCreateTime);
        IPage<Orders> orderPage = ordersMapper.selectJoinPage(p, Orders.class, wrapper);
        List<MyOrderVO> voList = new ArrayList<>();
        if (orderPage != null && orderPage.getRecords() != null) {
            for (Orders o : orderPage.getRecords()) {
                MyOrderVO vo = new MyOrderVO();
                vo.setId(o.getId());
                vo.setCode(o.getCode());
                vo.setType(o.getType());
                vo.setStatus(o.getStatus());
                vo.setCreateTime(o.getCreateTime());
                vo.setExpectedTakeTime(o.getExpectedTakeTime());
                // 存件门店(关联查询直接取值)
                vo.setDepositShopName(o.getDepositShopName());
                vo.setDepositShopLinkName(o.getDepositShopLinkName());
                vo.setDepositShopPhone(o.getDepositShopLinkPhone());
                // 取件信息:有取件门店取门店,无则取用户自选取件点
                if (o.getTakeShopId() != null) {
                    vo.setTakeShopName(o.getTakeShopName());
                    vo.setTakeShopAddress(o.getTakeShopAddress());
                } else {
                    vo.setTakeLocation(o.getTakeLocation());
                    vo.setTakeLocationRemark(o.getTakeLocationRemark());
                }
                // 取件联系人
                vo.setTakeUser(o.getTakeUser());
                vo.setTakePhone(o.getTakePhone());
                // 费用(分)
                vo.setDeclaredFee(o.getDeclaredFee());
                vo.setEstimatedAmount(o.getEstimatedAmount());
                // 查询物品明细(一次查询,同时用于物品列表和逾期计算)
                List<OrdersDetail> details = ordersDetailMapper.selectList(
                        new QueryWrapper<OrdersDetail>().lambda()
                                .eq(OrdersDetail::getOrderId, o.getId())
                                .eq(OrdersDetail::getDeleted, Constants.ZERO));
                // 物品明细
                vo.setDetailList(buildDetailList(details));
                // 逾期信息(仅待取件状态计算)
                if (Integer.valueOf(Constants.OrderStatus.arrived.getStatus()).equals(o.getStatus())) {
                    OverdueFeeVO overdueInfo = calculateOverdueFeeInternal(o, details);
                    vo.setOverdue(overdueInfo.getOverdue());
                    vo.setOverdueDays(overdueInfo.getOverdueDays());
                    vo.setOverdueFee(overdueInfo.getOverdueFee());
                }
                voList.add(vo);
            }
        }
        IPage<MyOrderVO> vPage = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        PageData<MyOrderVO> pageData = PageData.from(vPage);
        pageData.setRecords(voList);
        pageData.setTotal(orderPage.getTotal());
        pageData.setPage(orderPage.getCurrent());
        pageData.setCapacity(orderPage.getSize());
        return pageData;
    }
    @Override
    public MyOrderDetailVO findMyOrderDetail(Integer id, Integer memberId) {
        Orders order = ordersMapper.selectOne(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getId, id)
                .eq(Orders::getMemberId, memberId)
                .eq(Orders::getDeleted, Constants.ZERO));
        if (order == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        return buildOrderDetailVO(order, true);
    }
    @Override
    public MyOrderDetailVO findShopOrderDetail(Integer orderId, String verifyCode) {
        Orders order = null;
        if (orderId != null) {
            order = ordersMapper.selectOne(new QueryWrapper<Orders>().lambda()
                    .eq(Orders::getId, orderId)
                    .eq(Orders::getDeleted, Constants.ZERO));
        } else if (StringUtils.isNotBlank(verifyCode)) {
            order = ordersMapper.selectOne(new QueryWrapper<Orders>().lambda()
                    .eq(Orders::getMemberVerifyCode, verifyCode)
                    .eq(Orders::getDeleted, Constants.ZERO)
                    .last("limit 1"));
            if (order == null) {
                order = ordersMapper.selectOne(new QueryWrapper<Orders>().lambda()
                        .eq(Orders::getDriverVerifyCode, verifyCode)
                        .eq(Orders::getDeleted, Constants.ZERO)
                        .last("limit 1"));
            }
        }
        if (order == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在");
        }
        return buildOrderDetailVO(order, false);
    }
    /**
     * 构建订单详情VO(会员端/门店端复用)
     *
     * @param order          订单实体
     * @param memberViewMode true=会员端(按条件返回核销码),false=门店端(始终返回核销码)
     */
    private MyOrderDetailVO buildOrderDetailVO(Orders order, boolean memberViewMode) {
        MyOrderDetailVO vo = new MyOrderDetailVO();
        vo.setId(order.getId());
        vo.setStatus(order.getStatus());
        vo.setType(order.getType());
        vo.setCode(order.getCode());
        vo.setOutTradeNo(order.getOutTradeNo());
        vo.setRemark(order.getRemark());
        vo.setCreateTime(order.getCreateTime());
        vo.setPayTime(order.getPayTime());
        vo.setExpectedDepositTime(order.getExpectedDepositTime());
        vo.setExpectedTakeTime(order.getExpectedTakeTime());
        vo.setArriveTime(order.getArriveTime());
        // 费用(分)
        vo.setBasicAmount(order.getBasicAmount());
        vo.setDeclaredAmount(order.getDeclaredAmount());
        vo.setDeclaredFee(order.getDeclaredFee());
        vo.setUrgentAmount(order.getUrgentAmount());
        vo.setActualPayAmount(order.getPayAmount());
        // 标记
        vo.setExceptionStatus(order.getExceptionStatus());
        // 是否超出取件时间
        vo.setPastTakeTime(order.getExpectedTakeTime() != null && new Date().after(order.getExpectedTakeTime()));
        // 订单状态描述 + 倒计时
        vo.setStatusDesc(buildStatusDesc(order));
        if (Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.waitPay.getStatus())) {
            vo.setPayCountdownMs(calcPayCountdownMs(order));
        }
        // 存件门店
        if (order.getDepositShopId() != null) {
            ShopInfo depositShop = shopInfoMapper.selectById(order.getDepositShopId());
            if (depositShop != null) {
                vo.setDepositShopName(depositShop.getName());
                vo.setDepositShopLinkName(depositShop.getLinkName());
                vo.setDepositShopPhone(depositShop.getLinkPhone());
                vo.setDepositShopAddress(depositShop.getAddress());
            }
        }
        // 取件信息
        if (order.getTakeShopId() != null) {
            ShopInfo takeShop = shopInfoMapper.selectById(order.getTakeShopId());
            if (takeShop != null) {
                vo.setTakeShopName(takeShop.getName());
                vo.setTakeShopAddress(takeShop.getAddress());
            }
        } else {
            vo.setTakeLocation(order.getTakeLocation());
            vo.setTakeLocationRemark(order.getTakeLocationRemark());
        }
        // 取件联系人
        vo.setTakeUser(order.getTakeUser());
        vo.setTakePhone(order.getTakePhone());
        // 物品类型名称
        if (order.getGoodType() != null) {
            Category category = categoryMapper.selectById(order.getGoodType());
            if (category != null) {
                vo.setGoodTypeName(category.getName());
            }
        }
        // 下单照片
        String imgPrefix = getOrdersPrefix();
        vo.setOrderImages(getFileUrls(order.getId(), Constants.FileType.ORDER_FILE.getKey(), imgPrefix));
        // 物品明细
        List<OrdersDetail> details = ordersDetailMapper.selectList(
                new QueryWrapper<OrdersDetail>().lambda()
                        .eq(OrdersDetail::getOrderId, order.getId())
                        .eq(OrdersDetail::getDeleted, Constants.ZERO));
        vo.setDetailList(buildDetailList(details));
        // 逾期信息
        OverdueFeeVO overdueInfo = calculateOverdueFeeInternal(order, details);
        vo.setOverdue(overdueInfo.getOverdue());
        vo.setOverdueDays(overdueInfo.getOverdueDays());
        vo.setOverdueFee(overdueInfo.getOverdueFee());
        // 核销码
        Integer status = order.getStatus();
        if (memberViewMode) {
            // 会员端:待寄存(1)返回;待取件(5)时,就地寄存无取件门店不返回
            boolean returnCode = false;
            if (Constants.equalsInteger(status, Constants.OrderStatus.waitDeposit.getStatus())) {
                returnCode = true;
            } else if (Constants.equalsInteger(status, Constants.OrderStatus.arrived.getStatus())) {
                if (!(Constants.equalsInteger(order.getType(), Constants.ZERO) && order.getTakeShopId() == null)) {
                    returnCode = true;
                }
            }
            if (returnCode) {
                vo.setMemberVerifyCode(order.getMemberVerifyCode());
            }
        } else {
            // 门店端:始终返回会员核销码
            vo.setMemberVerifyCode(order.getMemberVerifyCode());
        }
        return vo;
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void cancelOrder(Integer orderId, Integer memberId, String reason) {
        Orders order = ordersMapper.selectOne(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getId, orderId)
                .eq(Orders::getMemberId, memberId)
                .eq(Orders::getDeleted, Constants.ZERO));
        if (order == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        // 仅异地寄存可取消
        if (!Constants.equalsInteger(order.getType(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅异地寄存订单可取消");
        }
        Integer status = order.getStatus();
        if (status == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "订单状态异常");
        }
        Date now = new Date();
        // 待支付:直接取消
        if (Constants.equalsInteger(status, Constants.OrderStatus.waitPay.getStatus())) {
            order.setStatus(Constants.OrderStatus.cancelled.getStatus());
            order.setCancelTime(now);
            ordersMapper.updateById(order);
            saveCancelLog(order, "会员取消订单(待支付)", reason, memberId);
            return;
        }
        // 待寄存:直接取消,全额退款
        if (Constants.equalsInteger(status, Constants.OrderStatus.waitDeposit.getStatus())) {
            // 记录退款信息
            OrdersRefund refund = new OrdersRefund();
            refund.setOrderId(orderId);
            refund.setType(0); // 未寄存直接取消
            refund.setCancelInfo(reason);
            refund.setCreateTime(now);
            refund.setDeleted(Constants.ZERO);
            // 调用微信退款,全额退款
            String refundCode = wxMiniUtilService.wxRefund(order.getOutTradeNo(), order.getPayAmount(), order.getPayAmount());
            refund.setRefundCode(refundCode);
            refund.setRefundTime(new Date());
            ordersRefundMapper.insert(refund);
            order.setStatus(Constants.OrderStatus.cancelled.getStatus());
            order.setCancelTime(now);
            order.setRefundAmount(order.getPayAmount());
            ordersMapper.updateById(order);
            saveCancelLog(order, "会员取消订单(待寄存,全额退款)", reason, memberId);
            return;
        }
        // 已寄存/已接单:进入取消中状态
        if (Constants.equalsInteger(status, Constants.OrderStatus.deposited.getStatus())
                || Constants.equalsInteger(status, Constants.OrderStatus.accepted.getStatus())) {
            order.setStatus(Constants.OrderStatus.cancelling.getStatus());
            order.setCancelTime(now);
            ordersMapper.updateById(order);
            saveCancelLog(order, "会员申请取消订单(已寄存/已接单)", reason, memberId);
            return;
        }
        throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许取消");
    }
    /**
     * 保存取消订单操作日志
     */
    private void saveCancelLog(Orders order, String title, String reason, Integer memberId) {
        OrderLog log = new OrderLog();
        log.setOrderId(order.getId());
        log.setTitle(title);
        log.setLogInfo(reason);
        log.setObjType(Constants.ORDER_LOG_CANCEL);
        log.setOrderStatus(order.getStatus());
        log.setOptUserId(memberId);
        log.setOptUserType(0); // 0=用户
        log.setCreateTime(new Date());
        log.setDeleted(Constants.ZERO);
        orderLogService.create(log);
    }
    /**
     * 保存门店核销日志
     */
    private void saveShopVerifyLog(Orders order, String title, String logInfo, String remark, Integer shopId) {
        OrderLog log = new OrderLog();
        log.setOrderId(order.getId());
        log.setTitle(title);
        log.setLogInfo(logInfo);
        log.setRemark(remark);
        log.setOrderStatus(order.getStatus());
        log.setOptUserId(shopId);
        log.setOptUserType(2); // 2=门店
        log.setCreateTime(new Date());
        log.setDeleted(Constants.ZERO);
        orderLogService.create(log);
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void handleStorageOrderPayNotify(String outTradeNo, String wxTradeNo) {
        Orders order = ordersMapper.selectOne(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getOutTradeNo, outTradeNo)
                .eq(Orders::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (order == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在: " + outTradeNo);
        }
        // 幂等:已支付则跳过
        if (Constants.equalsInteger(order.getPayStatus(), Constants.ONE)) {
            return;
        }
        Date now = new Date();
        order.setStatus(Constants.OrderStatus.waitDeposit.getStatus()); // 待寄存
        order.setPayStatus(Constants.ONE); // 已支付
        order.setPayTime(now);
        order.setWxExternalNo(wxTradeNo);
        order.setUpdateTime(now);
        // 生成会员核销码
        order.setMemberVerifyCode(generateVerifyCode());
        ordersMapper.updateById(order);
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void shopVerifyOrder(String verifyCode, Integer shopId, List<String> images, String remark) {
        if (StringUtils.isBlank(verifyCode)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "核销码不能为空");
        }
        // 根据核销码查找订单(会员核销码)
        Orders order = ordersMapper.selectOne(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getMemberVerifyCode, verifyCode)
                .eq(Orders::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (order == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "核销码无效");
        }
        // 查询门店名称用于日志
        String shopName = "";
        ShopInfo shopInfo = shopInfoMapper.selectById(shopId);
        if (shopInfo != null) {
            shopName = shopInfo.getName() != null ? shopInfo.getName() : "";
        }
        Integer status = order.getStatus();
        Date now = new Date();
        if (Constants.equalsInteger(status, Constants.OrderStatus.waitDeposit.getStatus())) {
            // 待寄存(1) → 已寄存(2),两种类型通用
            // 校验当前门店是否为订单的存件门店
            if (!shopId.equals(order.getDepositShopId())) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不属于当前门店,无法核销");
            }
            order.setStatus(Constants.OrderStatus.deposited.getStatus());
            order.setDepositTime(now);
            // 释放当前核销码,生成新的核销码供取件时使用
            releaseVerifyCode(verifyCode);
            order.setMemberVerifyCode(generateVerifyCode());
            ordersMapper.updateById(order);
            // 保存寄存图片(obj_type=2 订单寄存图片,最多3张)
            saveVerifyImages(order.getId(), images, Constants.FileType.ORDER_DEPOSIT.getKey(), shopId);
            // 记录订单日志
            saveShopVerifyLog(order, "门店确认寄存", "门店【" + shopName + "】确认寄存", remark, shopId);
        } else if (Constants.equalsInteger(status, Constants.OrderStatus.arrived.getStatus())) {
            // 异地寄存 + 无取件门店 → 无法核销(客户自取,无门店操作)
            if (Constants.equalsInteger(order.getType(), Constants.ONE) && order.getTakeShopId() == null) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单无取件门店,无法核销");
            }
            // 校验取件门店与当前登录门店一致
            if (!shopId.equals(order.getTakeShopId())) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不属于当前门店,无法核销");
            }
            // 待取件(5) → 已完成(7)
            order.setStatus(Constants.OrderStatus.finished.getStatus());
            order.setConfirmArriveTime(now);
            ordersMapper.updateById(order);
            // 订单完成,释放核销码
            releaseVerifyCode(verifyCode);
            // 保存出库图片(obj_type=13 门店出库图片,最多3张)
            saveVerifyImages(order.getId(), images, Constants.FileType.STORE_OUT.getKey(), shopId);
            // 记录订单日志
            saveShopVerifyLog(order, "门店确认取件", "门店【" + shopName + "】确认取件,订单完成", remark, shopId);
        } else {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许核销");
        }
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void driverVerifyOrder(String verifyCode, List<String> images, String remark, Integer driverId) {
        if (StringUtils.isBlank(verifyCode)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "核销码不能为空");
        }
        // 根据司机核销码查找订单
        Orders order = ordersMapper.selectOne(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getDriverVerifyCode, verifyCode)
                .eq(Orders::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (order == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "核销码无效");
        }
        // 仅异地寄存 + 有取件门店 + 派送中(4) 可核销
        if (!Constants.equalsInteger(order.getType(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅异地寄存订单支持司机核销");
        }
        if (order.getTakeShopId() == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单无取件门店,无需司机核销");
        }
        if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.delivering.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许核销");
        }
        // 派送中(4) → 已到店(5)
        order.setStatus(Constants.OrderStatus.arrived.getStatus());
        order.setArriveTime(new Date());
        if (StringUtils.isNotBlank(remark)) {
            order.setRemark(remark);
        }
        ordersMapper.updateById(order);
        // 释放司机核销码
        releaseVerifyCode(verifyCode);
        // 保存附件(obj_type=3 门店入库图片,最多3张)
        saveVerifyImages(order.getId(), images, Constants.FileType.ORDER_TAKE.getKey(), driverId);
    }
    /**
     * 保存核销附件到 multifile 表
     *
     * @param orderId  订单主键
     * @param images   图片地址列表(最多3张)
     * @param objType  附件类型
     * @param creator  创建人编码
     */
    private void saveVerifyImages(Integer orderId, List<String> images, int objType, Integer creator) {
        if (images == null || images.isEmpty()) return;
        List<String> saveImages = images.size() > 3 ? images.subList(0, 3) : images;
        Date now = new Date();
        int sortNum = 1;
        for (String imgUrl : saveImages) {
            if (StringUtils.isBlank(imgUrl)) continue;
            Multifile multifile = new Multifile();
            multifile.setObjId(orderId);
            multifile.setObjType(objType);
            multifile.setFileurl(imgUrl);
            multifile.setType(Constants.ZERO);
            multifile.setCreator(creator);
            multifile.setCreateDate(now);
            multifile.setIsdeleted(Constants.ZERO);
            multifile.setSortnum(sortNum++);
            multifileMapper.insert(multifile);
        }
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void confirmCustomerArrived(Integer orderId, Integer shopId) {
        // 1. 查询订单
        Orders order = ordersMapper.selectById(orderId);
        if (order == null || Constants.equalsInteger(order.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在");
        }
        // 2. 校验订单状态:待取件(5)
        if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.arrived.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许该操作");
        }
        // 3. 校验门店与订单关系
        if (Constants.equalsInteger(order.getType(), Constants.ONE) && order.getTakeShopId() != null) {
            // 异地寄存有取件门店:校验取件门店
            if (!shopId.equals(order.getTakeShopId())) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不属于当前门店");
            }
        } else if (Constants.equalsInteger(order.getType(), Constants.ZERO)) {
            // 就地寄存:校验存件门店(取件门店同存件门店)
            if (!shopId.equals(order.getDepositShopId())) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单不属于当前门店");
            }
        } else {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单无取件门店,无法确认到店");
        }
        // 4. 查询门店名称(用于日志)
        String shopName = "";
        ShopInfo shopInfo = shopInfoMapper.selectById(shopId);
        if (shopInfo != null) {
            shopName = shopInfo.getName() != null ? shopInfo.getName() : "";
        }
        // 5. 计算逾期费用
        List<OrdersDetail> details = ordersDetailMapper.selectList(
                new QueryWrapper<OrdersDetail>().lambda()
                        .eq(OrdersDetail::getOrderId, orderId)
                        .eq(OrdersDetail::getDeleted, Constants.ZERO));
        OverdueFeeVO overdueInfo = calculateOverdueFeeInternal(order, details);
        Date now = new Date();
        if (overdueInfo.getOverdue() && overdueInfo.getOverdueDays() > 0) {
            // 存在逾期:标记逾期状态,订单保持当前状态
            order.setConfirmArriveTime(now);
            order.setOverdueStatus(Constants.ONE);
            order.setOverdueDays(overdueInfo.getOverdueDays());
            order.setOverdueAmount(overdueInfo.getOverdueFee());
            order.setUpdateTime(now);
            ordersMapper.updateById(order);
            // 记录订单日志
            saveShopVerifyLog(order, "确认顾客到店(逾期)",
                    "门店【" + shopName + "】确认顾客到店,逾期" + overdueInfo.getOverdueDays()
                            + "天,逾期费用" + Constants.getFormatMoney(overdueInfo.getOverdueFee()) + "元",
                    null, shopId);
        } else {
            // 未逾期:完成订单
            order.setStatus(Constants.OrderStatus.finished.getStatus());
            order.setConfirmArriveTime(now);
            order.setFinishTime(now);
            order.setOverdueStatus(Constants.ZERO);
            order.setUpdateTime(now);
            ordersMapper.updateById(order);
            // 释放核销码
            if (StringUtils.isNotBlank(order.getMemberVerifyCode())) {
                releaseVerifyCode(order.getMemberVerifyCode());
            }
            // 记录订单日志
            saveShopVerifyLog(order, "确认顾客到店",
                    "门店【" + shopName + "】确认顾客到店,订单完成",
                    null, shopId);
        }
    }
    /**
     * 构建订单状态描述
     */
    private String buildStatusDesc(Orders order) {
        boolean isLocal = Constants.equalsInteger(order.getType(), Constants.ZERO);
        Integer status = order.getStatus();
        if (status == null) return "";
        if (Constants.equalsInteger(status, Constants.OrderStatus.waitPay.getStatus())) {
            String minutes = "";
            try {
                minutes = operationConfigBiz.getConfig().getAutoCancelTime();
            } catch (Exception ignored) {}
            return "请在" + (StringUtils.isNotBlank(minutes) ? minutes : "") + "分钟内完成支付,超时订单将自动取消";
        }
        if (Constants.equalsInteger(status, Constants.OrderStatus.waitDeposit.getStatus())) {
            return isLocal ? "订单已支付,请等待门店确认接单" : "订单已支付,请等待门店确认接单";
        }
        if (Constants.equalsInteger(status, Constants.OrderStatus.deposited.getStatus())) {
            return isLocal ? "行李已寄存,请凭取件码前往指定门店取件" : "门店已接单,正在为您安排取件司机";
        }
        if (Constants.equalsInteger(status, Constants.OrderStatus.accepted.getStatus())) {
            return isLocal ? "行李已寄存,请凭取件码前往指定门店取件" : "已有司机抢单,正前往取件地点";
        }
        if (Constants.equalsInteger(status, Constants.OrderStatus.delivering.getStatus())) {
            return "司机已取件,正运往目的地";
        }
        if (Constants.equalsInteger(status, Constants.OrderStatus.arrived.getStatus())) {
            return "行李已送达服务点,请及时前往取件";
        }
        if (Constants.equalsInteger(status, Constants.OrderStatus.finished.getStatus())) {
            if (Constants.equalsInteger(order.getCommentStatus(), Constants.ONE)) {
                return "感谢您的用心评价,祝您出行顺利,旅途愉快!";
            }
            return "订单已完成,感谢您的支持,请对本次服务做出评价";
        }
        if (Constants.equalsInteger(status, Constants.OrderStatus.cancelled.getStatus())) {
            return "订单已取消,感谢您的支持,欢迎下次再会!";
        }
        if (Constants.equalsInteger(status, Constants.OrderStatus.cancelling.getStatus())) {
            return "退款申请已提交,平台会尽快为您处理退款";
        }
        if (Constants.equalsInteger(status, Constants.OrderStatus.closed.getStatus())) {
            return "退款已成功原路返回,请注意查收";
        }
        return "";
    }
    /**
     * 计算支付倒计时毫秒
     */
    private Long calcPayCountdownMs(Orders order) {
        try {
            String minutesStr = operationConfigBiz.getConfig().getAutoCancelTime();
            if (StringUtils.isBlank(minutesStr)) return -1L;
            int minutes = Integer.parseInt(minutesStr);
            long deadline = order.getCreateTime().getTime() + minutes * 60 * 1000L;
            long remaining = deadline - System.currentTimeMillis();
            return remaining > 0 ? remaining : -1L;
        } catch (Exception e) {
            return -1L;
        }
    }
    public OverdueFeeVO calculateOverdueFee(Integer orderId) {
        Orders order = ordersMapper.selectById(orderId);
        if (order == null || Constants.equalsInteger(order.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        // 查询订单明细
        List<OrdersDetail> details = ordersDetailMapper.selectList(
                new QueryWrapper<OrdersDetail>().lambda()
                        .eq(OrdersDetail::getOrderId, orderId)
                        .eq(OrdersDetail::getDeleted, Constants.ZERO));
        return calculateOverdueFeeInternal(order, details);
    }
    /**
     * 逾期费用内部计算(不查库,接受预查询的数据)
     * 供分页等已查询明细的业务场景复用,避免重复查询
     */
    private OverdueFeeVO calculateOverdueFeeInternal(Orders order, List<OrdersDetail> details) {
        if (CollectionUtils.isEmpty(details)) {
            OverdueFeeVO vo = new OverdueFeeVO();
            vo.setOverdue(false);
            vo.setOverdueDays(0);
            vo.setOverdueFee(0L);
            vo.setDailyBaseFee(0L);
            return vo;
        }
        // 物品基础日费用 = Σ(单价 × 数量)
        long dailyBaseFee = 0L;
        for (OrdersDetail d : details) {
            dailyBaseFee += (d.getUnitPrice() != null ? d.getUnitPrice() : 0L)
                    * (d.getNum() != null ? d.getNum() : 0);
        }
        Date now = new Date();
        int overdueDays;
        long overdueFee;
        if (Constants.equalsInteger(order.getType(), Constants.ZERO)) {
            // ========== 就地寄存 ==========
            overdueDays = calcLocalOverdueDays(now, order.getExpectedTakeTime());
            overdueFee = (long) overdueDays * dailyBaseFee;
            OverdueFeeVO vo = new OverdueFeeVO();
            vo.setOverdue(overdueDays > 0);
            vo.setOverdueDays(overdueDays);
            vo.setOverdueFee(overdueFee);
            vo.setDailyBaseFee(dailyBaseFee);
            return vo;
        } else {
            // ========== 异地寄存 ==========
            // 条件:存在取件门店 且 订单处于已到店状态(5)
            if (order.getTakeShopId() == null
                    || !Constants.equalsInteger(order.getStatus(), Constants.FIVE)) {
                OverdueFeeVO vo = new OverdueFeeVO();
                vo.setOverdue(false);
                vo.setOverdueDays(0);
                vo.setOverdueFee(0L);
                vo.setDailyBaseFee(dailyBaseFee);
                vo.setDiscountRate(null);
                return vo;
            }
            overdueDays = calcRemoteOverdueDays(now, order.getArriveTime());
            // 折扣比率
            String discountStr = operationConfigBiz.getConfig().getUnpickedDiscount();
            BigDecimal discountRate = StringUtils.isNotBlank(discountStr)
                    ? new BigDecimal(discountStr) : BigDecimal.ONE;
            overdueFee = new BigDecimal(overdueDays)
                    .multiply(new BigDecimal(dailyBaseFee))
                    .multiply(discountRate)
                    .setScale(0, RoundingMode.HALF_UP)
                    .longValue();
            OverdueFeeVO vo = new OverdueFeeVO();
            vo.setOverdue(overdueDays > 0);
            vo.setOverdueDays(overdueDays);
            vo.setOverdueFee(overdueFee);
            vo.setDailyBaseFee(dailyBaseFee);
            vo.setDiscountRate(discountStr);
            return vo;
        }
    }
    /**
     * 就地寄存逾期天数计算
     * 过了预计取件时间当天的12点后才算一天
     */
    private int calcLocalOverdueDays(Date now, Date expectedTakeTime) {
        if (expectedTakeTime == null || !now.after(expectedTakeTime)) {
            return 0;
        }
        // 基准时间 = 预计取件日期的12:00
        Calendar baseCal = Calendar.getInstance();
        baseCal.setTime(expectedTakeTime);
        baseCal.set(Calendar.HOUR_OF_DAY, 12);
        baseCal.set(Calendar.MINUTE, 0);
        baseCal.set(Calendar.SECOND, 0);
        baseCal.set(Calendar.MILLISECOND, 0);
        Date baseTime = baseCal.getTime();
        if (!now.after(baseTime)) {
            return 0;
        }
        // 逾期天数 = 当前日期 - 基准日期(按天取差)
        Calendar nowCal = Calendar.getInstance();
        nowCal.setTime(now);
        nowCal.set(Calendar.HOUR_OF_DAY, 0);
        nowCal.set(Calendar.MINUTE, 0);
        nowCal.set(Calendar.SECOND, 0);
        nowCal.set(Calendar.MILLISECOND, 0);
        Calendar baseDateCal = Calendar.getInstance();
        baseDateCal.setTime(baseTime);
        baseDateCal.set(Calendar.HOUR_OF_DAY, 0);
        baseDateCal.set(Calendar.MINUTE, 0);
        baseDateCal.set(Calendar.SECOND, 0);
        baseDateCal.set(Calendar.MILLISECOND, 0);
        long diffMs = nowCal.getTimeInMillis() - baseDateCal.getTimeInMillis();
        int days = (int) (diffMs / (1000 * 60 * 60 * 24));
        return Math.max(days, 0);
    }
    /**
     * 异地寄存逾期天数计算
     * 过了转移到店时间当天的晚上12点(24:00)后才算第一天
     */
    private int calcRemoteOverdueDays(Date now, Date arriveTime) {
        if (arriveTime == null || !now.after(arriveTime)) {
            return 0;
        }
        // 基准时间 = 转移到店日期的次日 00:00(即当天24:00)
        Calendar baseCal = Calendar.getInstance();
        baseCal.setTime(arriveTime);
        baseCal.set(Calendar.HOUR_OF_DAY, 0);
        baseCal.set(Calendar.MINUTE, 0);
        baseCal.set(Calendar.SECOND, 0);
        baseCal.set(Calendar.MILLISECOND, 0);
        baseCal.add(Calendar.DAY_OF_MONTH, 1); // 次日00:00 = 当天24:00
        Date baseTime = baseCal.getTime();
        if (!now.after(baseTime)) {
            return 0;
        }
        // 逾期天数 = 当前日期 - 基准日期
        Calendar nowCal = Calendar.getInstance();
        nowCal.setTime(now);
        nowCal.set(Calendar.HOUR_OF_DAY, 0);
        nowCal.set(Calendar.MINUTE, 0);
        nowCal.set(Calendar.SECOND, 0);
        nowCal.set(Calendar.MILLISECOND, 0);
        Calendar baseDateCal = Calendar.getInstance();
        baseDateCal.setTime(baseTime);
        baseDateCal.set(Calendar.HOUR_OF_DAY, 0);
        baseDateCal.set(Calendar.MINUTE, 0);
        baseDateCal.set(Calendar.SECOND, 0);
        baseDateCal.set(Calendar.MILLISECOND, 0);
        long diffMs = nowCal.getTimeInMillis() - baseDateCal.getTimeInMillis();
        int days = (int) (diffMs / (1000 * 60 * 60 * 24));
        return Math.max(days, 0);
    }
}
server/services/src/main/java/com/doumee/service/business/impl/PricingRuleServiceImpl.java
@@ -34,6 +34,8 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.stream.Collectors;
@@ -381,63 +383,111 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveEstimatedDelivery(EstimatedDeliverySaveDTO request) {
        // 查询已有规则 type=2, fieldA=1, cityId, deleted=0
        QueryWrapper<PricingRule> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(PricingRule::getType, Constants.TWO)
                .eq(PricingRule::getFieldA, "1")
                .eq(PricingRule::getCityId, request.getCityId())
                .eq(PricingRule::getDeleted, Constants.ZERO)
                .last("limit 1");
        PricingRule existing = pricingRuleMapper.selectOne(qw);
        if (request.getItems() == null || request.getItems().isEmpty()) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "时效配置列表不能为空");
        }
        Date now = new Date();
        if (existing != null) {
            // 更新
            existing.setFieldB(request.getStartDistance());
            existing.setFieldC(request.getStartTime());
            existing.setFieldD(request.getContinueDistance());
            existing.setFieldE(request.getContinueTime());
            existing.setUpdateTime(now);
            pricingRuleMapper.updateById(existing);
        } else {
            // 新增
            PricingRule rule = new PricingRule();
            rule.setType(Constants.TWO);
            rule.setFieldA("1");
            rule.setFieldB(request.getStartDistance());
            rule.setFieldC(request.getStartTime());
            rule.setFieldD(request.getContinueDistance());
            rule.setFieldE(request.getContinueTime());
            rule.setCityId(request.getCityId());
            rule.setDeleted(Constants.ZERO);
            rule.setCreateTime(now);
            rule.setUpdateTime(now);
            pricingRuleMapper.insert(rule);
        for (EstimatedDeliverySaveDTO.EstimatedDeliveryItem item : request.getItems()) {
            // 查询已有规则 type=2, fieldA, cityId, deleted=0
            QueryWrapper<PricingRule> qw = new QueryWrapper<>();
            qw.lambda()
                    .eq(PricingRule::getType, Constants.TWO)
                    .eq(PricingRule::getFieldA, String.valueOf(item.getFieldA()))
                    .eq(PricingRule::getCityId, request.getCityId())
                    .eq(PricingRule::getDeleted, Constants.ZERO)
                    .last("limit 1");
            PricingRule existing = pricingRuleMapper.selectOne(qw);
            if (existing != null) {
                existing.setFieldB(item.getStartDistance());
                existing.setFieldC(item.getStartTime());
                existing.setFieldD(item.getContinueDistance());
                existing.setFieldE(item.getContinueTime());
                existing.setUpdateTime(now);
                pricingRuleMapper.updateById(existing);
            } else {
                PricingRule rule = new PricingRule();
                rule.setType(Constants.TWO);
                rule.setFieldA(String.valueOf(item.getFieldA()));
                rule.setFieldB(item.getStartDistance());
                rule.setFieldC(item.getStartTime());
                rule.setFieldD(item.getContinueDistance());
                rule.setFieldE(item.getContinueTime());
                rule.setCityId(request.getCityId());
                rule.setDeleted(Constants.ZERO);
                rule.setCreateTime(now);
                rule.setUpdateTime(now);
                pricingRuleMapper.insert(rule);
            }
        }
    }
    @Override
    public EstimatedDeliveryVO getEstimatedDelivery(Integer cityId) {
    public List<EstimatedDeliveryVO> getEstimatedDelivery(Integer cityId) {
        QueryWrapper<PricingRule> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(PricingRule::getType, Constants.TWO)
                .eq(PricingRule::getFieldA, "1")
                .in(PricingRule::getFieldA, "1", "2")
                .eq(PricingRule::getCityId, cityId)
                .eq(PricingRule::getDeleted, Constants.ZERO)
                .orderByAsc(PricingRule::getFieldA);
        List<PricingRule> rules = pricingRuleMapper.selectList(qw);
        // 将已有规则按fieldA分组
        Map<String, PricingRule> ruleMap = new HashMap<>();
        for (PricingRule rule : rules) {
            ruleMap.put(rule.getFieldA(), rule);
        }
        // 固定返回2条:fieldA=1(标速达), fieldA=2(极速达),无数据时返回空对象
        List<EstimatedDeliveryVO> voList = new ArrayList<>();
        for (int fieldA = 1; fieldA <= 2; fieldA++) {
            EstimatedDeliveryVO vo = new EstimatedDeliveryVO();
            vo.setCityId(cityId);
            vo.setFieldA(fieldA);
            PricingRule rule = ruleMap.get(String.valueOf(fieldA));
            if (rule != null) {
                vo.setPricingRuleId(rule.getId());
                vo.setStartDistance(rule.getFieldB());
                vo.setStartTime(rule.getFieldC());
                vo.setContinueDistance(rule.getFieldD());
                vo.setContinueTime(rule.getFieldE());
            }
            voList.add(vo);
        }
        return voList;
    }
    @Override
    public BigDecimal calculateEstimatedTime(Integer cityId, Integer fieldA, BigDecimal distance) {
        if (cityId == null || fieldA == null || distance == null) {
            return null;
        }
        QueryWrapper<PricingRule> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(PricingRule::getType, Constants.TWO)
                .eq(PricingRule::getFieldA, String.valueOf(fieldA))
                .eq(PricingRule::getCityId, cityId)
                .eq(PricingRule::getDeleted, Constants.ZERO)
                .last("limit 1");
        PricingRule rule = pricingRuleMapper.selectOne(qw);
        EstimatedDeliveryVO vo = new EstimatedDeliveryVO();
        vo.setCityId(cityId);
        if (rule != null) {
            vo.setPricingRuleId(rule.getId());
            vo.setStartDistance(rule.getFieldB());
            vo.setStartTime(rule.getFieldC());
            vo.setContinueDistance(rule.getFieldD());
            vo.setContinueTime(rule.getFieldE());
        if (rule == null) {
            return null;
        }
        return vo;
        // fieldB=起送里程, fieldC=起送时长, fieldD=续送里程, fieldE=续送时长
        BigDecimal startDistance = new BigDecimal(rule.getFieldB());
        BigDecimal startTime = new BigDecimal(rule.getFieldC());
        BigDecimal continueDistance = new BigDecimal(rule.getFieldD());
        BigDecimal continueTime = new BigDecimal(rule.getFieldE());
        // 距离 <= 起送里程 → 直接返回起送时长
        if (distance.compareTo(startDistance) <= 0) {
            return startTime;
        }
        // 超出部分:向上取整 * 续送时长 + 起送时长
        BigDecimal extraDistance = distance.subtract(startDistance);
        BigDecimal extraCount = extraDistance.divide(continueDistance, 0, RoundingMode.CEILING);
        return startTime.add(extraCount.multiply(continueTime));
    }
    @Override
server/services/src/main/java/com/doumee/service/business/impl/ShopInfoServiceImpl.java
@@ -1,9 +1,11 @@
package com.doumee.service.business.impl;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.doumee.config.jwt.JwtTokenUtil;
import com.doumee.core.constants.Constants;
import com.doumee.core.constants.ResponseStatus;
import com.doumee.core.exception.BusinessException;
@@ -19,17 +21,19 @@
import com.doumee.dao.business.model.Member;
import com.doumee.dao.business.model.Multifile;
import com.doumee.dao.business.model.ShopInfo;
import com.doumee.dao.dto.AuditDTO;
import com.doumee.dao.dto.ChangeStatusDTO;
import com.doumee.dao.dto.ResetPasswordDTO;
import com.doumee.dao.dto.ShopApplyDTO;
import com.doumee.dao.dto.ShopUpdateDTO;
import com.doumee.dao.dto.*;
import com.doumee.dao.system.SystemUserMapper;
import com.doumee.dao.system.model.SystemUser;
import com.doumee.dao.vo.ShopDetailVO;
import com.doumee.dao.vo.ShopLoginVO;
import com.doumee.dao.vo.ShopNearbyVO;
import com.doumee.dao.vo.ShopWebDetailVO;
import com.doumee.service.business.ShopInfoService;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -57,8 +61,13 @@
    private SystemDictDataBiz systemDictDataBiz;
    @Autowired
    private SystemUserMapper systemUserMapper;
    @Autowired
    private AreasBiz areasBiz;
    @Autowired
    private RedisTemplate<String,Object> redisTemplate;
    @Override
    public Integer create(ShopInfo shopInfo) {
        shopInfoMapper.insert(shopInfo);
@@ -175,6 +184,9 @@
        }
        if (pageWrap.getModel().getAuditStatus() != null) {
            queryWrapper.lambda().eq(ShopInfo::getAuditStatus, pageWrap.getModel().getAuditStatus());
        }
        if (pageWrap.getModel().getAuditStatusList() != null && !pageWrap.getModel().getAuditStatusList().isEmpty()) {
            queryWrapper.lambda().in(ShopInfo::getAuditStatus, pageWrap.getModel().getAuditStatusList());
        }
        if (pageWrap.getModel().getStatus() != null) {
            queryWrapper.lambda().eq(ShopInfo::getStatus, pageWrap.getModel().getStatus());
@@ -586,6 +598,11 @@
        vo.setAuditStatus(shopInfo.getAuditStatus());
        vo.setStatus(shopInfo.getStatus());
        vo.setAuditTime(shopInfo.getAuditTime());
        if(Objects.nonNull(shopInfo.getAuditUserId())){
            SystemUser systemUser = systemUserMapper.selectById(shopInfo.getAuditUserId());
            if (systemUser != null) vo.setAuditName(systemUser.getRealname());
        }
        vo.setAuditRemark(shopInfo.getAuditRemark());
        vo.setOpenid(shopInfo.getOpenid());
        vo.setPayStatus(shopInfo.getPayStatus());
@@ -602,10 +619,13 @@
        }
        vo.setImgPrefix(imgPrefix);
        // 单图字段返回半路径
        // 单图字段:半路径 + 全路径
        vo.setIdcardImg(shopInfo.getIdcardImg());
        vo.setIdcardImgUrl(StringUtils.isNotBlank(shopInfo.getIdcardImg()) ? imgPrefix + shopInfo.getIdcardImg() : null);
        vo.setIdcardImgBack(shopInfo.getIdcardImgBack());
        vo.setIdcardImgBackUrl(StringUtils.isNotBlank(shopInfo.getIdcardImgBack()) ? imgPrefix + shopInfo.getIdcardImgBack() : null);
        vo.setBusinessImg(shopInfo.getBusinessImg());
        vo.setBusinessImgUrl(StringUtils.isNotBlank(shopInfo.getBusinessImg()) ? imgPrefix + shopInfo.getBusinessImg() : null);
        // 查询附件
        QueryWrapper<Multifile> fileQw = new QueryWrapper<>();
@@ -621,10 +641,13 @@
                .orderByAsc(Multifile::getObjType, Multifile::getSortnum);
        List<Multifile> files = multifileMapper.selectList(fileQw);
        // 按 objType 分组,返回半路径
        // 按 objType 分组,半路径 + 全路径
        Map<Integer, List<String>> fileMap = new HashMap<>();
        Map<Integer, List<String>> fileUrlMap = new HashMap<>();
        for (Multifile f : files) {
            fileMap.computeIfAbsent(f.getObjType(), k -> new ArrayList<>()).add(f.getFileurl());
            String fullUrl = StringUtils.isNotBlank(f.getFileurl()) ? imgPrefix + f.getFileurl() : f.getFileurl();
            fileUrlMap.computeIfAbsent(f.getObjType(), k -> new ArrayList<>()).add(fullUrl);
        }
        vo.setStoreFrontImgs(fileMap.getOrDefault(Constants.FileType.STORE_FRONT.getKey(), new ArrayList<>()));
@@ -633,17 +656,341 @@
        vo.setLaborContractImgs(fileMap.getOrDefault(Constants.FileType.LABOR_CONTRACT.getKey(), new ArrayList<>()));
        vo.setSocialSecurityImgs(fileMap.getOrDefault(Constants.FileType.SOCIAL_SECURITY.getKey(), new ArrayList<>()));
        vo.setStoreFrontImgUrls(fileUrlMap.getOrDefault(Constants.FileType.STORE_FRONT.getKey(), new ArrayList<>()));
        vo.setStoreInteriorImgUrls(fileUrlMap.getOrDefault(Constants.FileType.STORE_INTERIOR.getKey(), new ArrayList<>()));
        vo.setOtherMaterialImgUrls(fileUrlMap.getOrDefault(Constants.FileType.OTHER_MATERIAL.getKey(), new ArrayList<>()));
        vo.setLaborContractImgUrls(fileUrlMap.getOrDefault(Constants.FileType.LABOR_CONTRACT.getKey(), new ArrayList<>()));
        vo.setSocialSecurityImgUrls(fileUrlMap.getOrDefault(Constants.FileType.SOCIAL_SECURITY.getKey(), new ArrayList<>()));
        // 查询绑定开户会员头像(payMemberOpenId 关联 member.openid)
        if (StringUtils.isNotBlank(shopInfo.getPayMemberOpenId())) {
            QueryWrapper<Member> memberQw = new QueryWrapper<>();
            memberQw.lambda().eq(Member::getOpenid, shopInfo.getPayMemberOpenId()).last("limit 1");
            Member payMember = memberMapper.selectOne(memberQw);
            if (payMember != null) {
                vo.setPayMemberCoverImage(payMember.getCoverImage());
            if (payMember != null && StringUtils.isNotBlank(payMember.getCoverImage())) {
                String memberPrefix = "";
                try {
                    memberPrefix = systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.RESOURCE_PATH).getCode()
                            + systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.MEMBER_FILES).getCode();
                } catch (Exception e) {
                    // 未配置时忽略
                }
                vo.setPayMemberCoverImage(memberPrefix + payMember.getCoverImage());
            }
        }
        return vo;
    }
    @Override
    public PageData<ShopNearbyVO> findNearbyShops(PageWrap<ShopNearbyDTO> pageWrap) {
        IPage<ShopInfo> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        ShopNearbyDTO dto = pageWrap.getModel();
        Double longitude = dto.getLongitude();
        Double latitude = dto.getLatitude();
        Integer sortType = dto.getSortType();
        Integer distanceMeter = dto.getDistance();
        // Haversine距离公式(单位km)
        String distanceFormula = "(6371 * acos(cos(radians(" + latitude + ")) * cos(radians(latitude)) " +
                "* cos(radians(longitude) - radians(" + longitude + ")) " +
                "+ sin(radians(" + latitude + ")) * sin(radians(latitude))))";
        QueryWrapper<ShopInfo> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(ShopInfo::getDeleted, Constants.ZERO)
                .eq(ShopInfo::getStatus, Constants.ZERO)
                .eq(ShopInfo::getAuditStatus, Constants.THREE);
        // 门店营业类型筛选
        if (dto.getBusinessType() != null) {
            qw.lambda().eq(ShopInfo::getBusinessType, dto.getBusinessType());
        }
        // 门店名称模糊查询
        if (StringUtils.isNotBlank(dto.getName())) {
            qw.lambda().like(ShopInfo::getName, dto.getName());
        }
        // 距离筛选(单位:米 → 转换为km比较)
        if (distanceMeter != null && distanceMeter > 0) {
            double maxKm = distanceMeter / 1000.0;
            qw.apply(distanceFormula + " <= {0}", maxKm);
        }
        // 排序
        if (longitude != null && latitude != null) {
            if (sortType != null && sortType == 2) {
                // 按评分降序
                qw.last("ORDER BY score DESC");
            } else {
                // 默认:按距离升序
                qw.last("ORDER BY " + distanceFormula + " ASC");
            }
        } else {
            qw.lambda().orderByDesc(ShopInfo::getCreateTime);
        }
        IPage<ShopInfo> result = shopInfoMapper.selectPage(page, qw);
        // 图片前缀
        String imgPrefix = getShopPrefix();
        List<ShopNearbyVO> voList = new ArrayList<>();
        for (ShopInfo shop : result.getRecords()) {
            ShopNearbyVO vo = new ShopNearbyVO();
            vo.setId(shop.getId());
            vo.setName(shop.getName());
            vo.setShopHours(shop.getShopHours());
            vo.setAddress(shop.getAddress());
            vo.setScore(shop.getScore());
            // 门头照第一张
            vo.setCoverImg(getFirstImage(shop.getId(), Constants.FileType.STORE_FRONT.getKey(), imgPrefix));
            // 距离
            if (longitude != null && latitude != null && shop.getLongitude() != null && shop.getLatitude() != null) {
                double distKm = haversine(latitude, longitude, shop.getLatitude(), shop.getLongitude());
                vo.setDistanceText(formatDistance(distKm));
            }
            voList.add(vo);
        }
        IPage<ShopNearbyVO> vPage = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        PageData<ShopNearbyVO> pageData = PageData.from(vPage);
        pageData.setRecords(voList);
        pageData.setTotal(result.getTotal());
        pageData.setPage(result.getCurrent());
        pageData.setCapacity(result.getSize());
        return pageData;
    }
    @Override
    public ShopWebDetailVO getShopWebDetail(ShopDetailQueryDTO dto) {
        ShopInfo shop = shopInfoMapper.selectById(dto.getId());
        if (Objects.isNull(shop) || Constants.equalsInteger(shop.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        ShopWebDetailVO vo = new ShopWebDetailVO();
        vo.setId(shop.getId());
        vo.setName(shop.getName());
        vo.setAddress(shop.getAddress());
        vo.setContent(shop.getContent());
        // 门头照 + 内部照 全路径集合
        String imgPrefix = getShopPrefix();
        List<String> images = new ArrayList<>();
        images.addAll(getImageList(dto.getId(), Constants.FileType.STORE_FRONT.getKey(), imgPrefix));
        images.addAll(getImageList(dto.getId(), Constants.FileType.STORE_INTERIOR.getKey(), imgPrefix));
        vo.setImages(images);
        // 距离
        if (dto.getLongitude() != null && dto.getLatitude() != null && shop.getLongitude() != null && shop.getLatitude() != null) {
            double distKm = haversine(dto.getLatitude(), dto.getLongitude(), shop.getLatitude(), shop.getLongitude());
            vo.setDistanceText(formatDistance(distKm));
        }
        return vo;
    }
    @Override
    public void maintainShopInfo(Integer memberId, ShopInfoMaintainDTO dto) {
        // 门店主键与会员主键一致
        ShopInfo shop = shopInfoMapper.selectById(memberId);
        if (Objects.isNull(shop) || Constants.equalsInteger(shop.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        // 需要支付完押金后才可维护(auditStatus >= 2)
        if (shop.getAuditStatus() == null || shop.getAuditStatus() < Constants.TWO) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "请先完成押金支付后再维护门店信息");
        }
        UpdateWrapper<ShopInfo> updateWrapper = new UpdateWrapper<>();
        updateWrapper.lambda()
                .eq(ShopInfo::getId, shop.getId())
                .set(ShopInfo::getUpdateTime, new Date())
                .set(dto.getCoverImg() != null, ShopInfo::getCoverImg, dto.getCoverImg())
                .set(dto.getContent() != null, ShopInfo::getContent, dto.getContent())
                .set(dto.getDepositTypes() != null, ShopInfo::getDepositTypes, dto.getDepositTypes())
                .set(dto.getFeeStandard() != null, ShopInfo::getFeeStandard, dto.getFeeStandard())
                .set(dto.getDeliveryArea() != null, ShopInfo::getDeliveryArea, dto.getDeliveryArea())
                .set(dto.getShopHours() != null, ShopInfo::getShopHours, dto.getShopHours())
                .set(dto.getBusinessType() != null, ShopInfo::getBusinessType, dto.getBusinessType());
        shopInfoMapper.update(updateWrapper);
    }
    @Override
    public ShopInfoMaintainDTO getShopMaintainInfo(Integer memberId) {
        ShopInfo shop = shopInfoMapper.selectById(memberId);
        if (Objects.isNull(shop) || Constants.equalsInteger(shop.getDeleted(), Constants.ONE)) {
            return null;
        }
        ShopInfoMaintainDTO dto = new ShopInfoMaintainDTO();
        dto.setCoverImg(shop.getCoverImg());
        dto.setContent(shop.getContent());
        dto.setDepositTypes(shop.getDepositTypes());
        dto.setFeeStandard(shop.getFeeStandard());
        dto.setDeliveryArea(shop.getDeliveryArea());
        dto.setShopHours(shop.getShopHours());
        dto.setBusinessType(shop.getBusinessType());
        return dto;
    }
    /**
     * Haversine公式计算两点间距离(km)
     */
    private double haversine(double lat1, double lng1, double lat2, double lng2) {
        double R = 6371;
        double dLat = Math.toRadians(lat2 - lat1);
        double dLng = Math.toRadians(lng2 - lng1);
        double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
                + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
                * Math.sin(dLng / 2) * Math.sin(dLng / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        return R * c;
    }
    /**
     * 格式化距离:小于1km显示米,大于等于1km显示千米(保留1位小数)
     */
    private String formatDistance(double km) {
        if (km < 1) {
            return Math.round(km * 1000) + "m";
        }
        return String.format("%.1fkm", km);
    }
    /**
     * 获取门店图片前缀
     */
    private String getShopPrefix() {
        try {
            return systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.RESOURCE_PATH).getCode()
                    + systemDictDataBiz.queryByCode(Constants.SYSTEM, Constants.SHOP_FILES).getCode();
        } catch (Exception e) {
            return "";
        }
    }
    /**
     * 获取门店指定类型的第一张图片全路径
     */
    private String getFirstImage(Integer shopId, int objType, String imgPrefix) {
        QueryWrapper<Multifile> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(Multifile::getObjId, shopId)
                .eq(Multifile::getObjType, objType)
                .eq(Multifile::getIsdeleted, Constants.ZERO)
                .orderByAsc(Multifile::getSortnum)
                .last("limit 1");
        Multifile f = multifileMapper.selectOne(qw);
        return f != null && StringUtils.isNotBlank(f.getFileurl()) ? imgPrefix + f.getFileurl() : null;
    }
    /**
     * 获取门店指定类型的所有图片全路径
     */
    private List<String> getImageList(Integer shopId, int objType, String imgPrefix) {
        QueryWrapper<Multifile> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(Multifile::getObjId, shopId)
                .eq(Multifile::getObjType, objType)
                .eq(Multifile::getIsdeleted, Constants.ZERO)
                .orderByAsc(Multifile::getSortnum);
        List<Multifile> files = multifileMapper.selectList(qw);
        List<String> urls = new ArrayList<>();
        for (Multifile f : files) {
            if (StringUtils.isNotBlank(f.getFileurl())) {
                urls.add(imgPrefix + f.getFileurl());
            }
        }
        return urls;
    }
    /**
     * 商户账号密码登录
     * @param dto
     * @return
             */
    @Override
    public ShopLoginVO shopPasswordLogin(ShopLoginDTO dto){
        if(StringUtils.isBlank(dto.getTelephone())
                || StringUtils.isBlank(dto.getPassword())
                || StringUtils.isBlank(dto.getOpenid())
        ){
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"用户名或密码不能为空");
        }
        ShopInfo shop = shopInfoMapper.selectOne(new QueryWrapper<ShopInfo>().lambda().eq(ShopInfo::getTelephone, dto.getTelephone())
                .eq(ShopInfo::getDeleted,Constants.ZERO)
                .last("limit 1")
        );
        if(shop==null){
            throw new BusinessException(ResponseStatus.ACCOUNT_INCORRECT);
        }
        //加密密码
        String pwd = Utils.Secure.encryptPassword( dto.getPassword(), shop.getSalt());
        if(!pwd.equals(shop.getPassword())){
            throw new BusinessException(ResponseStatus.ACCOUNT_INCORRECT);
        }
        // 更新当前登录会员的openid到门店
        if(StringUtils.isNotBlank(dto.getOpenid())){
            shopInfoMapper.update(null,new UpdateWrapper<ShopInfo>().lambda()
                    .set(ShopInfo::getOpenid,dto.getOpenid())
                    .eq(ShopInfo::getId,shop.getId())
            );
            // 清空其他门店的同一openid,保证唯一
            shopInfoMapper.update(null,new UpdateWrapper<ShopInfo>().lambda()
                    .set(ShopInfo::getOpenid,null)
                    .eq(ShopInfo::getOpenid,dto.getOpenid())
                    .ne(ShopInfo::getId,shop.getId())
            );
            shop.setOpenid(dto.getOpenid());
        }
        // 创建token(generateTokenForRedis 已自动清除该用户旧token,保证唯一有效)
        String token = JwtTokenUtil.generateTokenForRedis(shop.getId(), Constants.TWO, JSONObject.toJSONString(shop), redisTemplate);
        // 构建响应
        ShopLoginVO vo = new ShopLoginVO();
        vo.setToken(token);
        vo.setShopId(shop.getId());
        vo.setShopName(shop.getName());
        vo.setCompanyType(shop.getCompanyType());
        // 所属城市名称
        Areas area = areasBiz.resolveArea(shop.getAreaId());
        if (area != null) {
            vo.setCityName(area.getCityName());
        }
        return vo;
    }
    @Override
    public ShopLoginVO shopSilentLogin(String openid) {
        if (StringUtils.isBlank(openid)) {
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "openid不能为空");
        }
        ShopInfo shop = shopInfoMapper.selectOne(new QueryWrapper<ShopInfo>().lambda()
                .eq(ShopInfo::getOpenid, openid)
                .eq(ShopInfo::getDeleted, Constants.ZERO)
                .last("limit 1"));
        if (shop == null) {
            return null;
        }
        // 创建token(generateTokenForRedis 已自动清除该用户旧token,保证唯一有效)
        String token = JwtTokenUtil.generateTokenForRedis(shop.getId(), Constants.TWO, JSONObject.toJSONString(shop), redisTemplate);
        ShopLoginVO vo = new ShopLoginVO();
        vo.setToken(token);
        vo.setShopId(shop.getId());
        vo.setShopName(shop.getName());
        vo.setCompanyType(shop.getCompanyType());
        Areas area = areasBiz.resolveArea(shop.getAreaId());
        if (area != null) {
            vo.setCityName(area.getCityName());
        }
        return vo;
    }
}
server/services/src/main/java/com/doumee/service/business/impl/SmsrecordServiceImpl.java
@@ -1,5 +1,7 @@
package com.doumee.service.business.impl;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.CommonResponse;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -9,7 +11,10 @@
import com.doumee.core.exception.BusinessException;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.core.utils.DateUtil;
import com.doumee.core.utils.Strings;
import com.doumee.core.utils.Utils;
import com.doumee.core.utils.aliyun.ALiYunSmSUtil;
import com.doumee.dao.business.SmsrecordMapper;
import com.doumee.dao.business.model.Smsrecord;
import com.doumee.service.business.SmsrecordService;
@@ -17,8 +22,7 @@
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Objects;
import java.util.*;
/**
 * 短信验证码Service实现
@@ -156,4 +160,77 @@
        return smsrecordMapper.selectCount(wrapper);
    }
    /****************************************移动端接口开始********************************************************************/
    /**
     * 发送短信  目前只有短信验证码
     * @param memberId
     * @param phone
     */
    @Override
    public void sendSms(Integer memberId,String phone){
        smsrecordMapper.update(null,
                new UpdateWrapper<Smsrecord>()
                        .set("STATUS",2)
                        .eq("PHONE",phone)
                        .eq("STATUS",0)
        );
        String digits = Strings.randomNumeric(4);
        //发送验证码
        Map<String,Object> map = new HashMap<String,Object>();
        map.put("code",digits);
        CommonResponse response = ALiYunSmSUtil.sendMessage(phone,map);
        if(response.getHttpResponse().isSuccess()){
            JSONObject data = JSONObject.parseObject(response.getData());
            String returnCode = data.getString("Code");
            if(!returnCode.equals("OK")){
                throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"短信发送失败:" + data.getString("Message"));
            }
        }
        //存储短信验证码
        Smsrecord smsrecord = new Smsrecord();
        smsrecord.setCreateTime(new Date());
        smsrecord.setMemberId(memberId);
        smsrecord.setPhone(phone);
        smsrecord.setContent("验证码短信");
        smsrecord.setType(Constants.ZERO);
        smsrecord.setValidDate(DateUtil.afterMinutesDate(3));
        smsrecord.setCode(digits);
        smsrecord.setStatus(Constants.ZERO);
        smsrecord.setDeleted(Constants.ZERO);
        smsrecordMapper.insert(smsrecord);
    }
    /**
     * 验证码验证
     * @param memberId
     * @param phone
     * @param code
     */
    @Override
    public void verifyCode(Integer memberId,String phone,String code){
        Smsrecord smsrecord = smsrecordMapper.selectOne(new QueryWrapper<Smsrecord>()
                .eq("MEMBER_ID",memberId)
                .eq("PHONE",phone)
                .eq("CODE",code)
                .eq("TYPE",Constants.ZERO)
                .eq("STATUS",Constants.ZERO)
                .last(" limit 1 ")
        );
        if(Objects.isNull(smsrecord)){
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"验证码输入错误或已过期!");
        }
        if(smsrecord.getValidDate().getTime()<System.currentTimeMillis()){
            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(),"验证码已过期请重新获取!");
        }
        smsrecord.setStatus(Constants.ONE);
        smsrecord.setUpdateTime(new Date());
        smsrecordMapper.updateById(smsrecord);
    }
}
server/services/src/main/java/com/doumee/service/business/impl/WithdrawalOrdersServiceImpl.java
@@ -10,13 +10,23 @@
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.core.utils.Utils;
import com.doumee.dao.business.DriverInfoMapper;
import com.doumee.dao.business.ShopInfoMapper;
import com.doumee.dao.business.WithdrawalOrdersMapper;
import com.doumee.dao.business.model.DriverInfo;
import com.doumee.dao.business.model.ShopInfo;
import com.doumee.dao.business.model.WithdrawalOrders;
import com.doumee.dao.dto.WithdrawalApproveDTO;
import com.doumee.dao.system.SystemUserMapper;
import com.doumee.dao.system.model.SystemUser;
import com.doumee.service.business.WithdrawalOrdersService;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@@ -30,6 +40,15 @@
    @Autowired
    private WithdrawalOrdersMapper withdrawalOrdersMapper;
    @Autowired
    private SystemUserMapper systemUserMapper;
    @Autowired
    private ShopInfoMapper shopInfoMapper;
    @Autowired
    private DriverInfoMapper driverInfoMapper;
    @Override
    public Integer create(WithdrawalOrders withdrawalOrders) {
@@ -75,11 +94,19 @@
    @Override
    public WithdrawalOrders findById(Integer id) {
        WithdrawalOrders withdrawalOrders = withdrawalOrdersMapper.selectById(id);
        if (Objects.isNull(withdrawalOrders)) {
        WithdrawalOrders order = withdrawalOrdersMapper.selectById(id);
        if (Objects.isNull(order)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        return withdrawalOrders;
        // 查询审批人名称
        fillUpdateUserName(order);
        // 根据用户类型查询关联信息
        if (Constants.ONE.equals(order.getMemberType())) {
            fillMemberInfo(order);
        } else {
            fillShopInfo(order);
        }
        return order;
    }
    @Override
@@ -97,16 +124,52 @@
    @Override
    public PageData<WithdrawalOrders> findPage(PageWrap<WithdrawalOrders> pageWrap) {
        IPage<WithdrawalOrders> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        QueryWrapper<WithdrawalOrders> queryWrapper = new QueryWrapper<>();
        MPJLambdaWrapper<WithdrawalOrders> queryWrapper = new MPJLambdaWrapper<>();
        Utils.MP.blankToNull(pageWrap.getModel());
        pageWrap.getModel().setDeleted(Constants.ZERO);
        queryWrapper.lambda().eq(WithdrawalOrders::getDeleted, pageWrap.getModel().getDeleted());
        queryWrapper.lambda().like(StringUtils.isNotBlank(pageWrap.getModel().getOutBillNo()), WithdrawalOrders::getOutBillNo, pageWrap.getModel().getOutBillNo());
        queryWrapper.lambda().eq(pageWrap.getModel().getMemberId() != null, WithdrawalOrders::getMemberId, pageWrap.getModel().getMemberId());
        queryWrapper.lambda().eq(pageWrap.getModel().getStatus() != null, WithdrawalOrders::getStatus, pageWrap.getModel().getStatus());
        queryWrapper.lambda().eq(pageWrap.getModel().getType() != null, WithdrawalOrders::getType, pageWrap.getModel().getType());
        queryWrapper.lambda().ge(pageWrap.getModel().getCreateStartTime() != null, WithdrawalOrders::getCreateTime, Utils.Date.getStart(pageWrap.getModel().getCreateStartTime()));
        queryWrapper.lambda().le(pageWrap.getModel().getCreateEndTime() != null, WithdrawalOrders::getCreateTime, Utils.Date.getEnd(pageWrap.getModel().getCreateEndTime()));
        // 公共:审批人名称
        queryWrapper.selectAll(WithdrawalOrders.class)
                .selectAs(SystemUser::getUsername, WithdrawalOrders::getUpdateUserName)
                .leftJoin(SystemUser.class, SystemUser::getId, WithdrawalOrders::getUserId);
        // 根据用户类型关联不同表
        Integer memberType = pageWrap.getModel().getMemberType();
        if (Constants.ONE.equals(memberType)) {
            // 司机端:关联 DriverInfo 表
            queryWrapper.selectAs(DriverInfo::getName, WithdrawalOrders::getMemberName)
                    .selectAs(DriverInfo::getTelephone, WithdrawalOrders::getMemberTelephone)
                    .leftJoin(DriverInfo.class, DriverInfo::getMemberId, WithdrawalOrders::getMemberId);
        } else {
            // 店铺端 / 不筛选:关联 ShopInfo 表
            queryWrapper.selectAs(ShopInfo::getName, WithdrawalOrders::getShopName)
                    .selectAs(ShopInfo::getLinkName, WithdrawalOrders::getLinkName)
                    .leftJoin(ShopInfo.class, ShopInfo::getId, WithdrawalOrders::getMemberId,
                            ext -> ext.eq(ShopInfo::getDeleted, Constants.ZERO));
        }
        queryWrapper.eq(WithdrawalOrders::getDeleted, pageWrap.getModel().getDeleted());
        if (memberType != null) {
            queryWrapper.eq(WithdrawalOrders::getMemberType, memberType);
        }
        if (StringUtils.isNotBlank(pageWrap.getModel().getOutBillNo())) {
            queryWrapper.like(WithdrawalOrders::getOutBillNo, pageWrap.getModel().getOutBillNo());
        }
        if (pageWrap.getModel().getMemberId() != null) {
            queryWrapper.eq(WithdrawalOrders::getMemberId, pageWrap.getModel().getMemberId());
        }
        if (pageWrap.getModel().getStatus() != null) {
            queryWrapper.eq(WithdrawalOrders::getStatus, pageWrap.getModel().getStatus());
        }
        if (pageWrap.getModel().getType() != null) {
            queryWrapper.eq(WithdrawalOrders::getType, pageWrap.getModel().getType());
        }
        if (pageWrap.getModel().getCreateStartTime() != null) {
            queryWrapper.ge(WithdrawalOrders::getCreateTime, Utils.Date.getStart(pageWrap.getModel().getCreateStartTime()));
        }
        if (pageWrap.getModel().getCreateEndTime() != null) {
            queryWrapper.le(WithdrawalOrders::getCreateTime, Utils.Date.getEnd(pageWrap.getModel().getCreateEndTime()));
        }
        for (PageWrap.SortData sortData : pageWrap.getSorts()) {
            if (sortData.getDirection().equalsIgnoreCase(PageWrap.DESC)) {
                queryWrapper.orderByDesc(sortData.getProperty());
@@ -114,8 +177,12 @@
                queryWrapper.orderByAsc(sortData.getProperty());
            }
        }
        return PageData.from(withdrawalOrdersMapper.selectPage(page, queryWrapper));
        return PageData.from(withdrawalOrdersMapper.selectJoinPage(page, WithdrawalOrders.class, queryWrapper));
    }
    @Override
    public long count(WithdrawalOrders withdrawalOrders) {
@@ -123,4 +190,103 @@
        return withdrawalOrdersMapper.selectCount(wrapper);
    }
    @Override
    public Long totalAmount(PageWrap<WithdrawalOrders> pageWrap) {
        QueryWrapper<WithdrawalOrders> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("IFNULL(SUM(amount), 0) as amount");
        Utils.MP.blankToNull(pageWrap.getModel());
        queryWrapper.lambda().eq(WithdrawalOrders::getDeleted, Constants.ZERO);
        queryWrapper.lambda().in(WithdrawalOrders::getStatus, Arrays.asList(Constants.ZERO, Constants.ONE));
        if (pageWrap.getModel().getOutBillNo() != null) {
            queryWrapper.lambda().like(WithdrawalOrders::getOutBillNo, pageWrap.getModel().getOutBillNo());
        }
        if (pageWrap.getModel().getMemberId() != null) {
            queryWrapper.lambda().eq(WithdrawalOrders::getMemberId, pageWrap.getModel().getMemberId());
        }
        if (pageWrap.getModel().getStatus() != null) {
            queryWrapper.lambda().eq(WithdrawalOrders::getStatus, pageWrap.getModel().getStatus());
        }
        if (pageWrap.getModel().getType() != null) {
            queryWrapper.lambda().eq(WithdrawalOrders::getType, pageWrap.getModel().getType());
        }
        if (pageWrap.getModel().getCreateStartTime() != null) {
            queryWrapper.lambda().ge(WithdrawalOrders::getCreateTime, Utils.Date.getStart(pageWrap.getModel().getCreateStartTime()));
        }
        if (pageWrap.getModel().getCreateEndTime() != null) {
            queryWrapper.lambda().le(WithdrawalOrders::getCreateTime, Utils.Date.getEnd(pageWrap.getModel().getCreateEndTime()));
        }
        WithdrawalOrders result = withdrawalOrdersMapper.selectOne(queryWrapper);
        return result != null && result.getAmount() != null ? result.getAmount() : 0L;
    }
    @Override
    public void approve(WithdrawalApproveDTO dto) {
        if (dto == null || dto.getId() == null || dto.getStatus() == null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "审批参数不完整");
        }
        if (!Constants.ONE.equals(dto.getStatus()) && !Constants.TWO.equals(dto.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "审批结果仅支持通过或拒绝");
        }
        WithdrawalOrders order = withdrawalOrdersMapper.selectById(dto.getId());
        if (Objects.isNull(order)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        if (!Constants.ZERO.equals(order.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅申请中的提现订单可审批");
        }
        // 获取当前登录用户
        Integer currentUserId = getCurrentUserId();
        WithdrawalOrders update = new WithdrawalOrders();
        update.setId(dto.getId());
        update.setStatus(dto.getStatus());
        update.setUserId(currentUserId);
        update.setApproveTime(new Date());
        update.setApproveRemark(dto.getApproveRemark());
        update.setUpdateTime(new Date());
        withdrawalOrdersMapper.updateById(update);
    }
    private Integer getCurrentUserId() {
        com.doumee.core.model.LoginUserInfo user =
                (com.doumee.core.model.LoginUserInfo) org.apache.shiro.SecurityUtils.getSubject().getPrincipal();
        return user != null ? user.getId() : null;
    }
    private void fillUpdateUserName(WithdrawalOrders order) {
        if (order.getUserId() != null) {
            SystemUser user = systemUserMapper.selectById(order.getUserId());
            if (user != null) {
                order.setUpdateUserName(user.getUsername());
            }
        }
    }
    private void fillShopInfo(WithdrawalOrders order) {
        if (order.getMemberId() != null) {
            ShopInfo shop = shopInfoMapper.selectById(order.getMemberId());
            if (shop != null && !Constants.ONE.equals(shop.getDeleted())) {
                order.setShopInfo(shop);
                order.setShopName(shop.getName());
                order.setLinkName(shop.getLinkName());
            }
        }
    }
    private void fillMemberInfo(WithdrawalOrders order) {
        if (order.getMemberId() != null) {
            DriverInfo driver = driverInfoMapper.selectOne(
                    new QueryWrapper<DriverInfo>().lambda()
                            .eq(DriverInfo::getMemberId, order.getMemberId())
                            .eq(DriverInfo::getDeleted, Constants.ZERO)
                            .last("limit 1"));
            if (driver != null) {
                order.setMemberName(driver.getName());
                order.setMemberTelephone(driver.getTelephone());
            }
        }
    }
}
server/services/src/main/resources/application-dev.yml
@@ -94,5 +94,8 @@
upload:
  type: ftp
# 腾讯地图apikey
tencent_key: WE3BZ-HN6WS-ONDOH-62QCV-MNL6F-5NFNE
server/services/src/main/resources/application-pro.yml
@@ -91,4 +91,9 @@
  type: blob
qiwei:
  serviceurl: https://wecom-qyapi.unilever-china.com/
  serviceurl: https://wecom-qyapi.unilever-china.com/
# 腾讯地图apikey
tencent_key: WE3BZ-HN6WS-ONDOH-62QCV-MNL6F-5NFNE
server/services/src/main/resources/application-test.yml
@@ -113,4 +113,8 @@
  type: blob
qiwei:
  serviceurl: https://qyapi.weixin.qq.com
  serviceurl: https://qyapi.weixin.qq.com
# 腾讯地图apikey
tencent_key: WE3BZ-HN6WS-ONDOH-62QCV-MNL6F-5NFNE
server/web/src/main/java/com/doumee/api/web/AccountApi.java
@@ -5,15 +5,19 @@
import com.doumee.core.annotation.trace.Trace;
import com.doumee.core.model.ApiResponse;
import com.doumee.dao.business.model.Member;
import com.doumee.dao.dto.ShopLoginDTO;
import com.doumee.dao.dto.WxPhoneRequest;
import com.doumee.dao.vo.AccountResponse;
import com.doumee.dao.vo.ShopLoginVO;
import com.doumee.service.business.MemberService;
import com.doumee.service.business.ShopInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
@@ -33,9 +37,12 @@
    @Autowired
    private MemberService memberService;
    @Autowired
    private ShopInfoService shopInfoService;
    @Trace
    @ApiOperation(value = "微信授权", notes = "小程序端")
    @ApiOperation(value = "会员微信授权", notes = "小程序端")
    @GetMapping("/wxLogin")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", dataType = "String", name = "code", value = "微信code", required = true)
@@ -45,7 +52,7 @@
    }
    @ApiOperation(value = "授权手机号", notes = "小程序端")
    @ApiOperation(value = "会员授权手机号", notes = "小程序端")
    @PostMapping("/wxAuthPhone")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", dataType = "String", name = "code", value = "微信code", required = true)
@@ -54,20 +61,23 @@
        return  ApiResponse.success("操作成功",memberService.wxAuthPhone(wxPhoneRequest));
    }
    @LoginRequired
    @ApiOperation(value = "退出登录", notes = "小程序端")
    @GetMapping("/logOff")
    @ApiOperation(value = "门店账号密码登录", notes = "小程序端,门店用户通过手机号+密码登录")
    @PostMapping("/shopLogin")
    public ApiResponse<ShopLoginVO> shopLogin(@RequestBody @Validated ShopLoginDTO dto) {
        return ApiResponse.success("操作成功", shopInfoService.shopPasswordLogin(dto));
    }
    @ApiOperation(value = "门店静默登录", notes = "根据openid自动登录门店,未绑定则返回空")
    @GetMapping("/shopSilentLogin")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
            @ApiImplicitParam(paramType = "query", dataType = "String", name = "openid", value = "微信openid", required = true)
    })
    public ApiResponse logOff() {
        String token = this.getRequest().getHeader(JwtTokenUtil.HEADER_KEY);
        memberService.logOut(token,getMemberId());
        return  ApiResponse.success("操作成功");
    public ApiResponse<ShopLoginVO> shopSilentLogin(@RequestParam String openid) {
        return ApiResponse.success("操作成功", shopInfoService.shopSilentLogin(openid));
    }
    @LoginRequired
    @ApiOperation(value = "用户注销", notes = "小程序端")
    @ApiOperation(value = "退出登录", notes = "小程序端")
    @GetMapping("/logOut")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
@@ -78,6 +88,18 @@
        return  ApiResponse.success("操作成功");
    }
    @LoginRequired
    @ApiOperation(value = "用户注销", notes = "小程序端")
    @GetMapping("/logOff")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
    })
    public ApiResponse logOff() {
        String token = this.getRequest().getHeader(JwtTokenUtil.HEADER_KEY);
        memberService.logOff(token,getMemberId());
        return  ApiResponse.success("操作成功");
    }
server/web/src/main/java/com/doumee/api/web/AddrApi.java
@@ -33,10 +33,7 @@
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
    })
    public ApiResponse<List<Addr>> list() {
        Addr query = new Addr();
        query.setMemberId(getMemberId());
        query.setDeleted(0);
        return ApiResponse.success("查询成功", addrService.findList(query));
        return ApiResponse.success("查询成功", addrService.findListWithArea(getMemberId()));
    }
    @LoginRequired
@@ -46,7 +43,7 @@
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
    })
    public ApiResponse<Addr> findById(@PathVariable Integer id) {
        return ApiResponse.success("查询成功", addrService.findById(id));
        return ApiResponse.success("查询成功", addrService.findByIdWithArea(id));
    }
    @LoginRequired
@@ -56,9 +53,8 @@
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
    })
    public ApiResponse create(@RequestBody Addr addr) {
        addr.setMemberId(getMemberId());
        addrService.create(addr);
        return ApiResponse.success("操作成功");
        addrService.createByMember(addr, getMemberId());
        return ApiResponse.success("操作成功", addrService.findByIdWithArea(addr.getId()));
    }
    @LoginRequired
@@ -68,9 +64,8 @@
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
    })
    public ApiResponse updateById(@RequestBody Addr addr) {
        addr.setMemberId(getMemberId());
        addrService.updateById(addr);
        return ApiResponse.success("操作成功");
        addrService.updateByMember(addr, getMemberId());
        return ApiResponse.success("操作成功", addrService.findByIdWithArea(addr.getId()));
    }
    @LoginRequired
@@ -94,7 +89,7 @@
        Addr addr = new Addr();
        addr.setId(id);
        addr.setIsDefault(1);
        addrService.updateById(addr);
        addrService.updateByMember(addr, getMemberId());
        return ApiResponse.success("操作成功");
    }
}
server/web/src/main/java/com/doumee/api/web/ApiController.java
@@ -37,6 +37,11 @@
        return obj != null ? (Integer) obj : null;
    }
    protected Integer getShopId() {
        Object obj = this.getRequest().getAttribute(JwtTokenUtil.SHOP_ID);
        return obj != null ? (Integer) obj : null;
    }
    /**
     * 获取登录用户对象信息
     * @return
server/web/src/main/java/com/doumee/api/web/ConfigApi.java
@@ -3,20 +3,26 @@
import com.doumee.core.annotation.LoginRequired;
import com.doumee.core.annotation.trace.Trace;
import com.doumee.core.model.ApiResponse;
import com.doumee.dao.business.model.Areas;
import com.doumee.dao.business.model.Banner;
import com.doumee.dao.business.model.Category;
import com.doumee.dao.dto.CalculateLocalPriceDTO;
import com.doumee.dao.dto.CalculateRemotePriceDTO;
import com.doumee.dao.vo.AccountResponse;
import com.doumee.service.business.CategoryService;
import com.doumee.dao.vo.PriceCalculateVO;
import com.doumee.dao.vo.UserCenterVO;
import com.doumee.service.business.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -28,21 +34,97 @@
@Api(tags = "配置类接口")
@Trace(exclude = true)
@RestController
@RequestMapping("/web/orders")
@RequestMapping("/web/config")
@Slf4j
public class ConfigApi extends ApiController{
    @Autowired
    private CategoryService categoryService;
    @Autowired
    private AreasService areasService;
    @Autowired
    private BannerService bannerService;
    @Autowired
    private OrdersService ordersService;
    @Autowired
    private MemberService memberService;
    @ApiOperation(value = "获取分类列表", notes = "小程序端")
    @GetMapping("/getCategoryList")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", dataType = "Integer", name = "type", value = "类型:0=品种配置;1=车辆类型配置;2=餐标配置;3=手续费配置;", required = true)
            @ApiImplicitParam(paramType = "query", dataType = "Integer", name = "type", value = "类型:1=车辆类型;2=物品分类;3=物品等级;4=物品尺寸;", required = true)
    })
    public ApiResponse<List<Category>> getCategoryList(@RequestParam Integer type) {
        return  ApiResponse.success("操作成功",categoryService.getCategoryList(type));
    }
    @ApiOperation(value = "获取开放城市列表", notes = "返回已开放城市,含首字母,按首字母排序")
    @GetMapping("/getOpenCityList")
    public ApiResponse<List<Areas>> getOpenCityList() {
        return ApiResponse.success("操作成功", areasService.getOpenCityList());
    }
    @ApiOperation(value = "获取轮播图列表", notes = "根据位置返回轮播图,含图片全路径")
    @GetMapping("/getBannerList")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", dataType = "Integer", name = "position", value = "位置:0=会员端首页轮播;1=司机APP引导页;", required = true)
    })
    public ApiResponse<List<Banner>> getBannerList(@RequestParam Integer position) {
        return ApiResponse.success("操作成功", bannerService.findListByPosition(position));
    }
    @ApiOperation(value = "获取城市已开通物品尺寸", notes = "根据城市主键查询已开通的物品尺寸(category type=4)")
    @GetMapping("/getCitySizeList")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", dataType = "Integer", name = "cityId", value = "城市主键", required = true)
    })
    public ApiResponse<List<Category>> getCitySizeList(@RequestParam Integer cityId) {
        return ApiResponse.success("操作成功", categoryService.getCitySizeList(cityId));
    }
    @ApiOperation(value = "获取系统配置", notes = "小程序端")
    @GetMapping("/getPlatformAboutUs")
    public ApiResponse<UserCenterVO> getPlatformAboutUs() {
        return  ApiResponse.success("查询成功",memberService.getPlatformAboutUs());
    }
    @LoginRequired
    @ApiOperation(value = "计算保价费用", notes = "根据报价金额计算保价费用")
    @GetMapping("/calculateInsuranceFee")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", dataType = "BigDecimal", name = "declaredValue", value = "报价金额", required = true),
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true)
    })
    public ApiResponse<BigDecimal> calculateInsuranceFee(@RequestParam BigDecimal declaredValue) {
        return ApiResponse.success("操作成功", ordersService.calculateInsuranceFee(declaredValue));
    }
    @LoginRequired
    @ApiOperation(value = "计算就地存取预估费用", notes = "根据城市、天数、物品类型和数量计算就地存取预估费用")
    @PostMapping("/calculateLocalPrice")
    public ApiResponse<PriceCalculateVO> calculateLocalPrice(@RequestBody @Valid CalculateLocalPriceDTO dto) {
        return ApiResponse.success("操作成功", ordersService.calculateLocalPrice(dto));
    }
    @LoginRequired
    @ApiOperation(value = "计算异地存取预估费用", notes = "根据距离、物品类型和数量计算异地存取预估费用")
    @PostMapping("/calculateRemotePrice")
    public ApiResponse<PriceCalculateVO> calculateRemotePrice(@RequestBody @Valid CalculateRemotePriceDTO dto) {
        return ApiResponse.success("操作成功", ordersService.calculateRemotePrice(dto));
    }
}
server/web/src/main/java/com/doumee/api/web/MemberApi.java
@@ -3,25 +3,16 @@
import com.doumee.core.annotation.LoginRequired;
import com.doumee.core.annotation.trace.Trace;
import com.doumee.core.model.ApiResponse;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.IdentityInfo;
import com.doumee.dao.business.model.Member;
import com.doumee.dao.business.model.MemberRevenue;
import com.doumee.dao.dto.UpdMobileRequest;
import com.doumee.dao.dto.WithdrawalDTO;
import com.doumee.dao.dto.WxPhoneRequest;
import com.doumee.dao.vo.AccountResponse;
import com.doumee.dao.vo.UserCenterVO;
import com.doumee.service.business.IdentityInfoService;
import com.doumee.service.business.MemberService;
import com.doumee.service.business.SmsrecordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@@ -34,30 +25,16 @@
@Api(tags = "2、用户信息")
@Trace(exclude = true)
@RestController
@RequestMapping("/web/user")
@RequestMapping("/web/member")
@Slf4j
public class UserApi extends  ApiController{
public class MemberApi extends  ApiController{
    @Autowired
    private MemberService memberService;
//    @Autowired
//    private IdentityInfoService identityInfoService;
//    @Autowired
//    private MemberRevenueService memberRevenueService;
//
//    @Autowired
//    private WithdrawalOrdersService withdrawalOrdersService;
    @ApiOperation(value = "获取系统配置", notes = "小程序端")
    @GetMapping("/getPlatformAboutUs")
    public ApiResponse<UserCenterVO> getPlatformAboutUs() {
        return  ApiResponse.success("查询成功",memberService.getPlatformAboutUs());
    }
    @Autowired
    private SmsrecordService smsrecordService;
    @LoginRequired
    @ApiOperation(value = "获取个人信息", notes = "小程序端")
@@ -82,6 +59,32 @@
        return  ApiResponse.success("操作成功");
    }
    @LoginRequired
    @ApiOperation(value = "发送短信验证码", notes = "小程序端")
    @GetMapping("/sendSmsCode")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
            @ApiImplicitParam(paramType = "query", dataType = "String", name = "phone", value = "手机号码", required = true)
    })
    public ApiResponse sendSmsCode(@RequestParam String phone) {
        smsrecordService.sendSms(getMemberId(),phone);
        return  ApiResponse.success("操作成功");
    }
    @LoginRequired
    @ApiOperation(value = "验证短信验证码", notes = "小程序端")
    @GetMapping("/verifyCode")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
            @ApiImplicitParam(paramType = "query", dataType = "String", name = "phone", value = "手机号码", required = true),
            @ApiImplicitParam(paramType = "query", dataType = "String", name = "code", value = "验证码", required = true)
    })
    public ApiResponse verifyCode(@RequestParam String phone,@RequestParam String code) {
        smsrecordService.verifyCode(getMemberId(),phone,code);
        return  ApiResponse.success("操作成功");
    }
    @LoginRequired
    @ApiOperation("验证手机号")
server/web/src/main/java/com/doumee/api/web/OrdersApi.java
@@ -1,112 +1,148 @@
package com.doumee.api.web;
import com.doumee.core.annotation.LoginRequired;
import com.doumee.core.annotation.LoginShopRequired;
import com.doumee.core.annotation.trace.Trace;
import com.doumee.core.model.ApiResponse;
import com.doumee.dao.business.model.Areas;
import com.doumee.dao.business.model.Banner;
import com.doumee.dao.business.model.Category;
import com.doumee.dao.dto.CalculateLocalPriceDTO;
import com.doumee.dao.dto.CalculateRemotePriceDTO;
import com.doumee.dao.vo.AccountResponse;
import com.doumee.dao.vo.PriceCalculateVO;
import com.doumee.service.business.AreasService;
import com.doumee.service.business.BannerService;
import com.doumee.service.business.CategoryService;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.dto.CancelOrderDTO;
import com.doumee.dao.dto.CreateOrderDTO;
import com.doumee.dao.dto.DriverVerifyDTO;
import com.doumee.dao.dto.ShopVerifyDTO;
import com.doumee.dao.dto.MyOrderDTO;
import com.doumee.dao.vo.MyOrderDetailVO;
import com.doumee.dao.vo.MyOrderVO;
import com.doumee.dao.vo.OverdueFeeVO;
import com.doumee.dao.vo.PayResponse;
import com.doumee.service.business.OrdersService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
/**
 * Created by IntelliJ IDEA.
 * 订单接口
 *
 * @Author : Rk
 * @create 2025/7/15 15:49
 */
@Api(tags = "配置类接口")
@Api(tags = "订单接口")
@Trace(exclude = true)
@RestController
@RequestMapping("/web/config")
@RequestMapping("/web/order")
@Slf4j
public class ConfigApi extends ApiController{
    @Autowired
    private CategoryService categoryService;
    @Autowired
    private AreasService areasService;
    @Autowired
    private BannerService bannerService;
public class OrdersApi extends ApiController {
    @Autowired
    private OrdersService ordersService;
    @ApiOperation(value = "获取分类列表", notes = "小程序端")
    @GetMapping("/getCategoryList")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", dataType = "Integer", name = "type", value = "类型:1=车辆类型;2=物品分类;3=物品等级;4=物品尺寸;", required = true)
    })
    public ApiResponse<List<Category>> getCategoryList(@RequestParam Integer type) {
        return  ApiResponse.success("操作成功",categoryService.getCategoryList(type));
    }
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @ApiOperation(value = "获取开放城市列表", notes = "返回已开放城市,含首字母,按首字母排序")
    @GetMapping("/getOpenCityList")
    public ApiResponse<List<Areas>> getOpenCityList() {
        return ApiResponse.success("操作成功", areasService.getOpenCityList());
    }
    @ApiOperation(value = "获取轮播图列表", notes = "根据位置返回轮播图,含图片全路径")
    @GetMapping("/getBannerList")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", dataType = "Integer", name = "position", value = "位置:0=会员端首页轮播;1=司机APP引导页;", required = true)
    })
    public ApiResponse<List<Banner>> getBannerList(@RequestParam Integer position) {
        return ApiResponse.success("操作成功", bannerService.findListByPosition(position));
    }
    @ApiOperation(value = "获取城市已开通物品尺寸", notes = "根据城市主键查询已开通的物品尺寸(category type=4)")
    @GetMapping("/getCitySizeList")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", dataType = "Integer", name = "cityId", value = "城市主键", required = true)
    })
    public ApiResponse<List<Category>> getCitySizeList(@RequestParam Integer cityId) {
        return ApiResponse.success("操作成功", categoryService.getCitySizeList(cityId));
    @LoginRequired
    @ApiOperation(value = "创建订单", notes = "创建就地/异地寄存订单,返回微信支付参数")
    @PostMapping("/create")
    public ApiResponse<PayResponse> createOrder(@RequestBody @Valid CreateOrderDTO dto) {
        PayResponse payResponse = ordersService.createOrder(dto, getMemberId());
        if (Objects.nonNull(payResponse) && StringUtils.isNotBlank(payResponse.getLockKey())) {
            redisTemplate.delete(payResponse.getLockKey());
        }
        return ApiResponse.success("操作成功", payResponse);
    }
    @LoginRequired
    @ApiOperation(value = "计算保价费用", notes = "根据报价金额计算保价费用")
    @GetMapping("/calculateInsuranceFee")
    @ApiOperation(value = "继续支付", notes = "待支付订单重新唤起微信支付")
    @PostMapping("/continuePay/{orderId}")
    public ApiResponse<PayResponse> continuePay(@PathVariable Integer orderId) {
        return ApiResponse.success("操作成功", ordersService.continuePay(orderId, getMemberId()));
    }
    @LoginRequired
    @ApiOperation(value = "会员订单分页", notes = "小程序端,按状态筛选")
    @PostMapping("/myPage")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", dataType = "BigDecimal", name = "declaredValue", value = "报价金额", required = true),
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
    })
    public ApiResponse<PageData<MyOrderVO>> myPage(@RequestBody @Validated PageWrap<MyOrderDTO> pageWrap) {
        return ApiResponse.success("查询成功", ordersService.findMyOrderPage(pageWrap, getMemberId()));
    }
    @LoginRequired
    @ApiOperation(value = "会员订单详情", notes = "小程序端,查询当前会员的订单详情")
    @GetMapping("/detail/{orderId}")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "path", dataType = "Integer", name = "orderId", value = "订单主键", required = true),
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true)
    })
    public ApiResponse<BigDecimal> calculateInsuranceFee(@RequestParam BigDecimal declaredValue) {
        return ApiResponse.success("操作成功", ordersService.calculateInsuranceFee(declaredValue));
    public ApiResponse<MyOrderDetailVO> detail(@PathVariable Integer orderId) {
        return ApiResponse.success("查询成功", ordersService.findMyOrderDetail(orderId, getMemberId()));
    }
    @LoginRequired
    @ApiOperation(value = "计算就地存取预估费用", notes = "根据城市、天数、物品类型和数量计算就地存取预估费用")
    @PostMapping("/calculateLocalPrice")
    public ApiResponse<PriceCalculateVO> calculateLocalPrice(@RequestBody @Valid CalculateLocalPriceDTO dto) {
        return ApiResponse.success("操作成功", ordersService.calculateLocalPrice(dto));
    @ApiOperation(value = "会员取消订单", notes = "仅异地寄存订单可取消")
    @PostMapping("/cancel")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true)
    })
    public ApiResponse cancel(@RequestBody @Validated CancelOrderDTO dto) {
        ordersService.cancelOrder(dto.getOrderId(), getMemberId(), dto.getCancelReason());
        return ApiResponse.success("操作成功");
    }
    @LoginRequired
    @ApiOperation(value = "计算异地存取预估费用", notes = "根据距离、物品类型和数量计算异地存取预估费用")
    @PostMapping("/calculateRemotePrice")
    public ApiResponse<PriceCalculateVO> calculateRemotePrice(@RequestBody @Valid CalculateRemotePriceDTO dto) {
        return ApiResponse.success("操作成功", ordersService.calculateRemotePrice(dto));
    @ApiOperation(value = "查询超时费用", notes = "查询订单逾期天数和逾期费用")
    @GetMapping("/overdueFee/{orderId}")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "path", dataType = "Integer", name = "orderId", value = "订单主键", required = true),
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true)
    })
    public ApiResponse<OverdueFeeVO> overdueFee(@PathVariable Integer orderId) {
        return ApiResponse.success("查询成功", ordersService.calculateOverdueFee(orderId));
    }
    @LoginShopRequired
    @ApiOperation(value = "门店核销收件", notes = "门店通过核销码确认收件/取件")
    @PostMapping("/shopVerify")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "门店token值", required = true)
    })
    public ApiResponse shopVerify(@RequestBody @Validated ShopVerifyDTO dto) {
        ordersService.shopVerifyOrder(dto.getVerifyCode(), getShopId(), dto.getImages(), dto.getRemark());
        return ApiResponse.success("核销成功");
    }
    @LoginShopRequired
    @ApiOperation(value = "核销司机码", notes = "异地寄存且有取件门店的订单,通过司机核销码确认到店")
    @PostMapping("/driverVerify")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true)
    })
    public ApiResponse driverVerify(@RequestBody @Validated DriverVerifyDTO dto) {
        ordersService.driverVerifyOrder(dto.getVerifyCode(), dto.getImages(), dto.getRemark(), getShopId());
        return ApiResponse.success("核销成功");
    }
    @LoginShopRequired
    @ApiOperation(value = "门店订单详情", notes = "门店端查询订单详情,支持订单主键或核销码查询")
    @GetMapping("/shopDetail")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "门店token值", required = true),
            @ApiImplicitParam(paramType = "query", dataType = "Integer", name = "orderId", value = "订单主键"),
            @ApiImplicitParam(paramType = "query", dataType = "String", name = "verifyCode", value = "核销码")
    })
    public ApiResponse<MyOrderDetailVO> shopDetail(@RequestParam(required = false) Integer orderId,
                                                    @RequestParam(required = false) String verifyCode) {
        return ApiResponse.success("查询成功", ordersService.findShopOrderDetail(orderId, verifyCode));
    }
server/web/src/main/java/com/doumee/api/web/PaymentCallback.java
@@ -1,16 +1,14 @@
package com.doumee.api.web.mall;
package com.doumee.api.web;
import com.doumee.api.web.ApiController;
import com.doumee.core.utils.Constants;
import com.doumee.config.wx.WxMiniConfig;
import com.doumee.core.constants.Constants;
import com.doumee.core.utils.ID;
import com.doumee.core.wx.WxMiniConfig;
import com.doumee.dao.business.model.ActivitySign;
import com.doumee.dao.business.model.Fund;
import com.doumee.dao.business.model.Goodsorder;
import com.doumee.service.business.OrdersService;
import com.github.binarywang.wxpay.bean.notify.WxPayNotifyResponse;
import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -30,6 +28,9 @@
@CrossOrigin
public class PaymentCallback extends ApiController {
    @Autowired
    private OrdersService ordersService;
    @PostMapping("/web/api/wxPayNotify")
    public String wxPay_notify(@RequestBody String xmlResult) {
@@ -45,102 +46,26 @@
            //微信订单号
            String paymentNo = result.getTransactionId();
            if (Constants.SUCCESS_STR.equals(result.getReturnCode())) {
            if (Constants.SUCCESS.equals(result.getReturnCode())) {
                // 支付成功
                switch (result.getAttach()) {
                    //活动参与支付
                    case "ActivitySign": {
                        ActivitySign activitySign = activitySignService.findById(Integer.valueOf(outTradeNo));
                        if(Objects.isNull(activitySign)){
                            return WxPayNotifyResponse.fail( "支付回调信息("+ wxId + ") = > 未查询到支付对象信息!");
                        }
                        if(activitySign.getStatus().equals(Constants.ONE)){
                            return WxPayNotifyResponse.success("处理成功!");
                        }
                        activitySign.setPayStatus(Constants.ONE);
                        activitySign.setPayDate(new Date());
                        activitySign.setPayOrderId(paymentNo);
                        activitySign.setStatus(Constants.ONE);
                        activitySignService.updateById(activitySign);
                    //寄存订单
                    case "storageOrder": {
                        ordersService.handleStorageOrderPayNotify(outTradeNo, paymentNo);
                        break;
                    }
                    case "terraceMall": {
                        Goodsorder DBGoodsOrder = new Goodsorder();
                        DBGoodsOrder.setCode(Long.valueOf(outTradeNo));
                        Goodsorder goodsOrder = goodsorderService.findOne(DBGoodsOrder);
                        if(Objects.isNull(goodsOrder)){
                            return WxPayNotifyResponse.fail( "支付回调信息("+ wxId + ") = > 未查询到支付对象信息!");
                        }
                        if(goodsOrder.getStatus().equals(Constants.ONE)){
                            return WxPayNotifyResponse.success("处理成功!");
                        }
                        goodsOrder.setPayStatus(Constants.ONE);
                        goodsOrder.setPayDate(new Date());
                        goodsOrder.setPayOrderId(paymentNo);
                        goodsOrder.setStatus(Constants.OrderStatus.PAY_DONE.getKey());
                        goodsOrder.setPayMethod(Constants.ZERO);
                        goodsorderService.updateById(goodsOrder);
                        //生成 咖啡计划订单明细表
                        if(goodsOrder.getType().equals(Constants.TWO)){
                            planorderDetailService.createPlanOrderDetail(goodsOrder);
                        }
                        Fund fund = new Fund();
                        fund.setOrderCode(goodsOrder.getPayOrderId());
                        fund.setCreator(goodsOrder.getMemberId());
                        fund.setCreateDate(new Date());
                        fund.setIsdeleted(Constants.ZERO);
                        fund.setRemark(goodsOrder.getCode().toString());
                        fund.setMemberId(goodsOrder.getMemberId());
                        fund.setTitle("订单支付");
                        fund.setContent("订单支付");
                        fund.setObjId(goodsOrder.getId());
                        fund.setObjType(Constants.ONE);
                        fund.setType(Constants.ZERO);
                        fund.setNum(goodsOrder.getPrice());
                        fundService.create(fund);
                        break;
                    }
                    case "shopGoods": {
                        Goodsorder DBGoodsOrder = new Goodsorder();
                        DBGoodsOrder.setCode(Long.valueOf(outTradeNo));
                        Goodsorder goodsOrder = goodsorderService.findOne(DBGoodsOrder);
                        if(Objects.isNull(goodsOrder)){
                            return WxPayNotifyResponse.fail( "支付回调信息("+ wxId + ") = > 未查询到支付对象信息!");
                        }
                        if(goodsOrder.getStatus().equals(Constants.ONE)){
                            return WxPayNotifyResponse.success("处理成功!");
                        }
                        goodsOrder.setPayStatus(Constants.ONE);
                        goodsOrder.setPayDate(new Date());
                        goodsOrder.setPayOrderId(paymentNo);
                        goodsOrder.setStatus(Constants.OrderStatus.PAY_DONE.getKey());
                        //生成核销码
                        if(Constants.equalsInteger(goodsOrder.getReceiveType(),Constants.ONE)){
                            goodsOrder.setExchangeCode(goodsorderService.createExchangeCode());
                        }
                        goodsOrder.setPayMethod(Constants.ZERO);
                        goodsorderService.updateById(goodsOrder);
                        if(Objects.nonNull(goodsOrder.getPickUpShopId())){
                            //发送站内信 - 经销商
                            noticeService.orderPayNotice(goodsOrder.getPickUpShopId(),goodsOrder.getId(),goodsOrder.getReceiveType());
                        }
                        Fund fund = new Fund();
                        fund.setOrderCode(goodsOrder.getPayOrderId());
                        fund.setCreator(goodsOrder.getMemberId());
                        fund.setCreateDate(new Date());
                        fund.setIsdeleted(Constants.ZERO);
                        fund.setRemark(goodsOrder.getCode().toString());
                        fund.setMemberId(goodsOrder.getMemberId());
                        fund.setTitle("订单支付");
                        fund.setContent("订单支付");
                        fund.setObjId(goodsOrder.getId());
                        fund.setObjType(Constants.ONE);
                        fund.setType(Constants.ZERO);
                        fund.setNum(goodsOrder.getPrice());
                        fundService.create(fund);
                        break;
                    }
                    //店铺押金订单
                    case "shopDeposit": {
                        break;
                    }
                    //逾期费用订单
                    case "overdueFee": {
                        break;
                    }
                }
                return WxPayNotifyResponse.success("处理成功!");
            }
server/web/src/main/java/com/doumee/api/web/ShopInfoApi.java
@@ -1,10 +1,18 @@
package com.doumee.api.web;
import com.doumee.core.annotation.LoginRequired;
import com.doumee.core.annotation.LoginShopRequired;
import com.doumee.core.model.ApiResponse;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.Member;
import com.doumee.dao.dto.ShopApplyDTO;
import com.doumee.dao.dto.ShopDetailQueryDTO;
import com.doumee.dao.dto.ShopInfoMaintainDTO;
import com.doumee.dao.dto.ShopNearbyDTO;
import com.doumee.dao.vo.ShopDetailVO;
import com.doumee.dao.vo.ShopNearbyVO;
import com.doumee.dao.vo.ShopWebDetailVO;
import com.doumee.service.business.ShopInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -41,4 +49,31 @@
        return ApiResponse.success(shopInfoService.getMyShop(this.getMemberId()));
    }
    @ApiOperation("附近门店分页列表")
    @PostMapping("/nearby")
    public ApiResponse<PageData<ShopNearbyVO>> nearby(@RequestBody @Validated PageWrap<ShopNearbyDTO> pageWrap) {
        return ApiResponse.success(shopInfoService.findNearbyShops(pageWrap));
    }
    @ApiOperation("门店详情")
    @PostMapping("/detail")
    public ApiResponse<ShopWebDetailVO> detail(@RequestBody @Validated ShopDetailQueryDTO dto) {
        return ApiResponse.success(shopInfoService.getShopWebDetail(dto));
    }
    @LoginShopRequired
    @ApiOperation("维护门店信息(支付押金后)")
    @PostMapping("/maintain")
    public ApiResponse maintain(@RequestBody ShopInfoMaintainDTO dto) {
        shopInfoService.maintainShopInfo(this.getMemberId(), dto);
        return ApiResponse.success("操作成功");
    }
    @LoginShopRequired
    @ApiOperation("查询门店维护信息")
    @PostMapping("/maintainInfo")
    public ApiResponse<ShopInfoMaintainDTO> maintainInfo() {
        return ApiResponse.success(shopInfoService.getShopMaintainInfo(this.getMemberId()));
    }
}