MrShi
10 小时以前 777bcb6c5660c0ed14d85207377b62b779d81dde
Merge branch 'master' of http://139.186.142.91:10010/r/productDev/gtzxinglijicun
已添加33个文件
已修改59个文件
4701 ■■■■■ 文件已修改
server/.claude/settings.json 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/admin/src/main/java/com/doumee/api/business/DataBoardController.java 57 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/admin/src/main/java/com/doumee/api/business/DriverInfoController.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/admin/src/main/java/com/doumee/api/business/InvoiceRecordController.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/admin/src/main/java/com/doumee/api/business/OrdersController.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/db/db_change.sql 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/biz/system/impl/OperationConfigBizImpl.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/core/constants/Constants.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/InvoiceRecordMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/RewardRecordMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/Category.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/DriverInfo.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/InvoiceRecord.java 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/Member.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/Notice.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/Orders.java 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/OrdersRefund.java 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/Revenue.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/RewardRecord.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/business/model/ShopInfo.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/ApplyInvoiceDTO.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/AuditDTO.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/CalculateLocalPriceDTO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/CalculateRemotePriceDTO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/ClaimRewardDTO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/CreateOrderDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/DataBoardQueryDTO.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/FinanceQueryDTO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/HandleOrderExceptionDTO.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/ManualRefundDTO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/MyOrderDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/OperationConfigDTO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/RevenueQueryDTO.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/SendInvoiceEmailDTO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/ShopNearbyDTO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/ShopRevenueQueryDTO.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/ShopUpdateDTO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/TrendQueryDTO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/invoice/InvoiceA0001DTO.java 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/invoice/InvoiceA0001ItemDTO.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/invoice/InvoiceA0001Request.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/invoice/InvoiceA0001Result.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/invoice/InvoiceA0001ResultItem.java 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/dto/invoice/InvoiceA0001ResultItemDetail.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/DataBoardVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/DriverKpiVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/DriverOrderTrendVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/DriverRewardHallVO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/DriverTopVO.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/FinanceOverviewVO.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/InvoiceRecordSummaryVO.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/LocationTagShopCountVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/LuggageTypeItem.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/MemberListVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/MyOrderVO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/OrderDetailVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/OrderSummaryVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/OrdersExportVO.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/PriceCalculateVO.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/ShopDetailVO.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/ShopKpiVO.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/ShopNearbyVO.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/ShopTopVO.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/ShopWebDetailVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/dao/vo/UserCenterVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/DataBoardService.java 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/InvoiceRecordService.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/MemberCouponService.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/OrdersService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/RevenueService.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/ShopInfoService.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/CategoryServiceImpl.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/DataBoardServiceImpl.java 576 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/DriverInfoServiceImpl.java 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/InvoiceRecordServiceImpl.java 353 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/MemberCouponServiceImpl.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/MemberServiceImpl.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/OrdersRefundServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/OrdersServiceImpl.java 775 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/RevenueServiceImpl.java 236 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/business/impl/ShopInfoServiceImpl.java 263 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/java/com/doumee/service/common/EmailService.java 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/resources/application-dev.yml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/resources/application-pro.yml 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/services/src/main/resources/application-test.yml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/ConfigApi.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/InvoiceApi.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/MemberCouponApi.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/OrdersApi.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/PaymentCallback.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/RevenueApi.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/web/src/main/java/com/doumee/api/web/ShopInfoApi.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/.claude/settings.json
@@ -1,7 +1,10 @@
{
  "permissions": {
    "allow": [
      "WebSearch"
      "WebSearch",
      "Bash(mvn package *)",
      "Bash(curl -s -X POST \"https://api.hydzfp.com/server/index.php?s=/api/page/info\" -d \"page_id=17\")",
      "Bash(curl -s -X POST \"https://api.hydzfp.com/server/index.php?s=/api/page/info\" -d \"page_id=20\")"
    ],
    "additionalDirectories": [
      "d:\\DouMee\\javaCodeGit\\gtzxinglijicun\\server\\services\\src\\main\\java\\com\\doumee\\core\\utils"
server/admin/src/main/java/com/doumee/api/business/DataBoardController.java
@@ -1,8 +1,11 @@
package com.doumee.api.business;
import com.doumee.api.BaseController;
import com.doumee.core.annotation.excel.ExcelExporter;
import com.doumee.core.model.ApiResponse;
import com.doumee.dao.dto.DataBoardQueryDTO;
import com.doumee.dao.dto.FinanceQueryDTO;
import com.doumee.dao.dto.TrendQueryDTO;
import com.doumee.dao.vo.*;
import com.doumee.service.business.DataBoardService;
import io.swagger.annotations.Api;
@@ -13,6 +16,9 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@Api(tags = "数据看板")
@@ -29,22 +35,22 @@
        return ApiResponse.success(dataBoardService.overview(query));
    }
    @ApiOperation("近30天会员注册趋势")
    @ApiOperation("会员注册趋势")
    @PostMapping("/memberTrend")
    public ApiResponse<List<MemberTrendVO>> memberTrend() {
        return ApiResponse.success(dataBoardService.memberTrend());
    public ApiResponse<List<MemberTrendVO>> memberTrend(@RequestBody TrendQueryDTO query) {
        return ApiResponse.success(dataBoardService.memberTrend(query));
    }
    @ApiOperation("近30天订单趋势")
    @ApiOperation("订单趋势")
    @PostMapping("/orderTrend")
    public ApiResponse<List<OrderTrendVO>> orderTrend() {
        return ApiResponse.success(dataBoardService.orderTrend());
    public ApiResponse<List<OrderTrendVO>> orderTrend(@RequestBody TrendQueryDTO query) {
        return ApiResponse.success(dataBoardService.orderTrend(query));
    }
    @ApiOperation("近30天营收趋势")
    @ApiOperation("营收趋势")
    @PostMapping("/revenueTrend")
    public ApiResponse<List<RevenueTrendVO>> revenueTrend() {
        return ApiResponse.success(dataBoardService.revenueTrend());
    public ApiResponse<List<RevenueTrendVO>> revenueTrend(@RequestBody TrendQueryDTO query) {
        return ApiResponse.success(dataBoardService.revenueTrend(query));
    }
    @ApiOperation("门店业绩统计")
@@ -52,4 +58,37 @@
    public ApiResponse<ShopPerformanceVO> shopPerformance(@RequestBody DataBoardQueryDTO query) {
        return ApiResponse.success(dataBoardService.shopPerformance(query));
    }
    @ApiOperation("行李类型占比导出")
    @PostMapping("/luggageTypeExport")
    public void luggageTypeExport(@RequestBody DataBoardQueryDTO query, HttpServletResponse response) {
        ExcelExporter.build(LuggageTypeItem.class)
                .export(dataBoardService.luggageTypeList(query), "行李类型占比", response);
    }
    @ApiOperation("平台财务总览")
    @PostMapping("/financeOverview")
    public ApiResponse<List<FinanceOverviewVO>> financeOverview(@RequestBody FinanceQueryDTO query) {
        return ApiResponse.success(dataBoardService.financeOverview(query));
    }
    @ApiOperation("平台财务总览导出")
    @PostMapping("/financeOverviewExport")
    public void financeOverviewExport(@RequestBody FinanceQueryDTO query, HttpServletResponse response) {
        String fileName = "平台财务数据报表_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
        ExcelExporter.build(FinanceOverviewVO.class)
                .export(dataBoardService.financeOverview(query), fileName, response);
    }
    @ApiOperation("门店业绩Top10")
    @PostMapping("/shopTop")
    public ApiResponse<List<ShopTopVO>> shopTop(@RequestBody TrendQueryDTO query) {
        return ApiResponse.success(dataBoardService.shopTop(query));
    }
    @ApiOperation("司机业绩Top10")
    @PostMapping("/driverTop")
    public ApiResponse<List<DriverTopVO>> driverTop(@RequestBody TrendQueryDTO query) {
        return ApiResponse.success(dataBoardService.driverTop(query));
    }
}
server/admin/src/main/java/com/doumee/api/business/DriverInfoController.java
@@ -18,7 +18,9 @@
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
@@ -83,7 +85,8 @@
    @RequiresPermissions("business:driverInfo:exportExcel")
    public void exportExcel(@RequestBody PageWrap<DriverInfo> pageWrap, HttpServletResponse response) {
        List<DriverInfo> driverInfoList = driverInfoService.findPage(pageWrap).getRecords();
        ExcelExporter.build(DriverInfo.class).export(driverInfoList, "司机注册信息", response);
        String fileName = "司机管理导出_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
        ExcelExporter.build(DriverInfo.class).export(driverInfoList, fileName, response);
    }
    @ApiOperation("根据ID查询")
server/admin/src/main/java/com/doumee/api/business/InvoiceRecordController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
package com.doumee.api.business;
import com.doumee.api.BaseController;
import com.doumee.core.annotation.pr.PreventRepeat;
import com.doumee.core.model.ApiResponse;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.InvoiceRecord;
import com.doumee.dao.vo.InvoiceRecordSummaryVO;
import com.doumee.service.business.InvoiceRecordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
/**
 * å‘票申请记录管理
 *
 * @author rk
 * @date 2026/05/18
 */
@Api(tags = "发票申请记录")
@RestController
@RequestMapping("/business/invoiceRecord")
public class InvoiceRecordController extends BaseController {
    @Autowired
    private InvoiceRecordService invoiceRecordService;
    @PreventRepeat
    @ApiOperation("新建")
    @PostMapping("/create")
    @RequiresPermissions("business:invoiceRecord:create")
    public ApiResponse create(@RequestBody InvoiceRecord invoiceRecord) {
        return ApiResponse.success(invoiceRecordService.create(invoiceRecord));
    }
    @ApiOperation("根据ID删除")
    @GetMapping("/delete/{id}")
    @RequiresPermissions("business:invoiceRecord:delete")
    public ApiResponse deleteById(@PathVariable Integer id) {
        invoiceRecordService.deleteById(id);
        return ApiResponse.success(null);
    }
    @ApiOperation("批量删除")
    @GetMapping("/delete/batch")
    @RequiresPermissions("business:invoiceRecord:delete")
    public ApiResponse deleteByIdInBatch(@RequestParam String ids) {
        String[] idArray = ids.split(",");
        List<Integer> idList = new ArrayList<>();
        for (String id : idArray) {
            idList.add(Integer.valueOf(id));
        }
        invoiceRecordService.deleteByIdInBatch(idList);
        return ApiResponse.success(null);
    }
    @ApiOperation("根据ID修改")
    @PostMapping("/updateById")
    @RequiresPermissions("business:invoiceRecord:update")
    public ApiResponse updateById(@RequestBody InvoiceRecord invoiceRecord) {
        invoiceRecordService.updateById(invoiceRecord);
        return ApiResponse.success(null);
    }
    @ApiOperation("分页查询")
    @PostMapping("/page")
    public ApiResponse<PageData<InvoiceRecord>> findPage(@RequestBody PageWrap<InvoiceRecord> pageWrap) {
        return ApiResponse.success(invoiceRecordService.findPage(pageWrap));
    }
    @ApiOperation("根据ID查询")
    @GetMapping("/{id}")
    public ApiResponse findById(@PathVariable Integer id) {
        return ApiResponse.success(invoiceRecordService.findById(id));
    }
    @ApiOperation("发票统计汇总")
    @PostMapping("/summary")
    public ApiResponse<InvoiceRecordSummaryVO> summary(@RequestBody PageWrap<InvoiceRecord> pageWrap) {
        return ApiResponse.success(invoiceRecordService.findPageSummary(pageWrap));
    }
}
server/admin/src/main/java/com/doumee/api/business/OrdersController.java
@@ -2,25 +2,35 @@
import com.doumee.api.BaseController;
import com.doumee.core.annotation.excel.ExcelExporter;
import com.doumee.core.constants.Constants;
import com.doumee.core.model.ApiResponse;
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.core.annotation.pr.PreventRepeat;
import com.doumee.dao.dto.DispatchDTO;
import com.doumee.dao.dto.HandleOrderExceptionDTO;
import com.doumee.dao.dto.ManualRefundDTO;
import com.doumee.dao.vo.OrderDetailVO;
import com.doumee.dao.vo.OrderDispatchVO;
import com.doumee.dao.vo.OrdersExportVO;
import com.doumee.dao.vo.OrderSummaryVO;
import com.doumee.service.business.OrdersService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
/**
 * å¯„存订单管理
@@ -82,7 +92,31 @@
    @PostMapping("/exportExcel")
    @RequiresPermissions("business:orders:exportExcel")
    public void exportExcel(@RequestBody PageWrap<Orders> pageWrap, HttpServletResponse response) {
        ExcelExporter.build(Orders.class).export(ordersService.findPage(pageWrap).getRecords(), "寄存订单", response);
        List<Orders> records = ordersService.findPage(pageWrap).getRecords();
        List<OrdersExportVO> exportList = new ArrayList<>();
        for (Orders o : records) {
            OrdersExportVO vo = new OrdersExportVO();
            vo.setCode(o.getCode());
            vo.setGoodsInfo(o.getGoodsInfo());
            vo.setTypeName(o.getType() != null ? (o.getType() == 0 ? "就地存取" : "异地存取") : "");
            vo.setOrderLevel(Constants.getDriverLevelName(Objects.nonNull(o.getOrderLevel())?Integer.valueOf(o.getOrderLevel()):Constants.ZERO));
            vo.setDeclaredFee(String.valueOf(Constants.getFormatMoney(o.getDeclaredFee())));
            vo.setBasicAmount(String.valueOf(Constants.getFormatMoney(o.getBasicAmount())));
            vo.setTotalAmount(String.valueOf(Constants.getFormatMoney(o.getTotalAmount())));
            vo.setPayAmount(String.valueOf(Constants.getFormatMoney(o.getPayAmount())));
            vo.setUrgentAmount(String.valueOf(Constants.getFormatMoney(o.getUrgentAmount())));
            vo.setRefundAmount(String.valueOf(Constants.getFormatMoney(o.getRefundAmount())));
            vo.setOverdueAmount(String.valueOf(Constants.getFormatMoney(o.getOverdueAmount())));
            vo.setExceptionAmount(String.valueOf(Constants.getFormatMoney(o.getExceptionAmount())));
            vo.setDeductionAmount(String.valueOf(Constants.getFormatMoney(o.getDeductionAmount())));
            vo.setStatusDesc(o.getStatusDesc());
            vo.setSettlementDesc(o.getSettlementStatus() != null ? (o.getSettlementStatus() == 1 ? "已结算" : "待结算") : "");
            vo.setPayTime(o.getPayTime());
            vo.setCreateTime(o.getCreateTime());
            exportList.add(vo);
        }
        String fileName = "订单管理导出_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
        ExcelExporter.build(OrdersExportVO.class).export(exportList, fileName, response);
    }
    @ApiOperation("根据ID查询")
@@ -122,4 +156,22 @@
        return ApiResponse.success("结算完成");
    }
    @ApiOperation("手动退款")
    @PostMapping("/manualRefund")
    @RequiresPermissions("business:orders:update")
    @PreventRepeat
    public ApiResponse manualRefund(@RequestBody @Valid ManualRefundDTO dto) {
        ordersService.manualRefund(dto, this.getLoginUser().getId());
        return ApiResponse.success("操作成功");
    }
    @PreventRepeat
    @ApiOperation("订单异常处理")
    @PostMapping("/handleException")
    @RequiresPermissions("business:orders:update")
    public ApiResponse handleException(@RequestBody @Valid HandleOrderExceptionDTO dto) {
        ordersService.handleOrderException(dto);
        return ApiResponse.success("操作成功");
    }
}
server/services/db/db_change.sql
@@ -2,7 +2,101 @@
-- æ•°æ®åº“变更脚本(全部变更统一记录在此文件)
-- æŒ‰æ—¥æœŸå€’序排列,每次变更用日期注释块分隔
-- ============================================================
-- 2026/05/18 è¿è¥é…ç½®å¢žåŠ å‘ç¥¨å¼€å…·æœˆä»½é™åˆ¶
-- ============================================================
INSERT INTO `system_dict_data` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `DELETED`)
VALUES ((SELECT `id` FROM `system_dict` WHERE `code` = 'OPERATION_CONFIG'), '12', 'INVOICE_MONTH_LIMIT', '发票开具月份限制(月)', 0, 0, 1, NOW(), 0);
-- ============================================================
-- 2026/05/18 è¿è¥é…ç½®å¢žåŠ æ³¨å†Œå®Œæˆè®¢å•å¥–åŠ±ã€å¹³å°å®Œæˆè®¢å•å¥–åŠ±
-- ============================================================
INSERT INTO `system_dict_data` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `DELETED`)
VALUES ((SELECT `id` FROM `system_dict` WHERE `code` = 'OPERATION_CONFIG'), '0', 'REGISTER_REWARD_ORDER_COUNT', '注册完成订单奖励单数', 0, 0, 1, NOW(), 0);
INSERT INTO `system_dict_data` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `DELETED`)
VALUES ((SELECT `id` FROM `system_dict` WHERE `code` = 'OPERATION_CONFIG'), '0', 'REGISTER_REWARD_AMOUNT', '注册完成订单奖励金额(分)', 0, 0, 1, NOW(), 0);
INSERT INTO `system_dict_data` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `DELETED`)
VALUES ((SELECT `id` FROM `system_dict` WHERE `code` = 'OPERATION_CONFIG'), '0', 'PLATFORM_REWARD_ORDER_COUNT', '平台完成订单奖励单数', 0, 0, 1, NOW(), 0);
INSERT INTO `system_dict_data` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `DELETED`)
VALUES ((SELECT `id` FROM `system_dict` WHERE `code` = 'OPERATION_CONFIG'), '0', 'PLATFORM_REWARD_AMOUNT', '平台完成订单奖励金额(分)', 0, 0, 1, NOW(), 0);
-- ============================================================
-- 2026/05/18 è®¢å•表增加门店异常补偿金额字段
-- ============================================================
ALTER TABLE `orders` ADD COLUMN `SHOP_COMPENSATION_AMOUNT` bigint DEFAULT NULL COMMENT '门店异常补偿金额(分)' AFTER `INVOICE_STATUS`;
-- ============================================================
-- 2026/05/18 å‘票记录表增加发票号码、订单编号、开票邮箱、发票PDF key字段
-- ============================================================
ALTER TABLE `invoice_record` ADD COLUMN `ORDER_NO` varchar(50) DEFAULT NULL COMMENT '订单编号' AFTER `MEMBER_ID`;
ALTER TABLE `invoice_record` ADD COLUMN `INVOICE_NO` varchar(50) DEFAULT NULL COMMENT '发票号码' AFTER `ORDER_NO`;
ALTER TABLE `invoice_record` ADD COLUMN `EMAIL` varchar(100) DEFAULT NULL COMMENT '开票邮箱' AFTER `FILE_ADDR`;
ALTER TABLE `invoice_record` ADD COLUMN `INVOICE_PDF_KEY` varchar(200) DEFAULT NULL COMMENT '发票PDF文件KEY' AFTER `EMAIL`;
-- ============================================================
-- 2026/05/18 è®¢å•增加发票状态字段 & å‘票申请记录表
-- ============================================================
ALTER TABLE `orders` ADD COLUMN `INVOICE_STATUS` int DEFAULT NULL COMMENT '发票状态:0=不可申请;1=可申请;2=申请中;3=开具成功;99=开具失败' AFTER `MANUAL_REFUND`;
CREATE TABLE `invoice_record` (
  `ID` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `DELETED` tinyint DEFAULT NULL COMMENT '是否已删除 0未删除 1已删除',
  `CREATE_USER` int DEFAULT NULL COMMENT '创建人编码',
  `CREATE_TIME` datetime DEFAULT NULL COMMENT '创建时间',
  `UPDATE_USER` int DEFAULT NULL COMMENT '更新人编码',
  `UPDATE_TIME` datetime DEFAULT NULL COMMENT '更新时间',
  `REMARK` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注',
  `ORDER_ID` int DEFAULT NULL COMMENT '订单主键(关联orders)',
  `MEMBER_ID` int DEFAULT NULL COMMENT '会员主键',
  `ORDER_NO` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '订单编号',
  `INVOICE_NO` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发票号码',
  `ORG_TYPE` int DEFAULT NULL COMMENT '组织类型:0=个人/事业单位;1=企业',
  `INVOICE_TYPE` int DEFAULT NULL COMMENT '发票类型:0=电子普通发票;1=电子专用发票',
  `NAME` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发票抬头',
  `TAX_ID` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '税号',
  `BANK_NAME` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '开户银行',
  `BANK_ACCOUNT` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '银行账号',
  `COMPANY_ADDR` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '企业地址(ORG_TYPE=1)',
  `COMPANY_PHONE` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '企业电话(ORG_TYPE=1)',
  `INVOICE_AMOUNT` bigint DEFAULT NULL COMMENT '发票金额',
  `STATUS` int DEFAULT NULL COMMENT '申请状态:0=申请中;1=开具成功;99=开具失败',
  `FILE_ADDR` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '发票文件地址',
  PRIMARY KEY (`ID`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='发票申请记录';
-- ============================================================
-- 2026/05/15 å¸æœºå¥–励规则配置
-- ============================================================
INSERT INTO `SYSTEM_DICT_DATA` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `DELETED`) VALUES (105, '', 'REGISTER_REWARD_ORDER_COUNT', '注册完成订单奖励单数', 0, 0, 1, NOW(), 0);
INSERT INTO `SYSTEM_DICT_DATA` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `DELETED`) VALUES (105, '', 'REGISTER_REWARD_AMOUNT', '注册完成订单奖励金额(分)', 0, 0, 1, NOW(), 0);
INSERT INTO `SYSTEM_DICT_DATA` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `DELETED`) VALUES (105, '', 'PLATFORM_REWARD_ORDER_COUNT', '平台完成订单奖励单数', 0, 0, 1, NOW(), 0);
INSERT INTO `SYSTEM_DICT_DATA` (`DICT_ID`, `CODE`, `LABEL`, `REMARK`, `SORT`, `DISABLED`, `CREATE_USER`, `CREATE_TIME`, `DELETED`) VALUES (105, '', 'PLATFORM_REWARD_AMOUNT', '平台完成订单奖励金额(分)', 0, 0, 1, NOW(), 0);
-- ============================================================
-- 2026/05/15 é—¨åº—表增加位置标签字段
-- ============================================================
ALTER TABLE `shop_info` ADD COLUMN `LOCATION_TAG_IDS` VARCHAR(500) NULL DEFAULT NULL COMMENT '位置标签主键(关联category type=5,多个以逗号分割)' AFTER `DEPOSIT_TYPES`;
-- ============================================================
-- 2026/05/15 é€€æ¬¾è¡¨å¢žåŠ æ‰£è´¹ä¿¡æ¯å­—æ®µ
-- ============================================================
ALTER TABLE `orders_refund` ADD COLUMN `DEDUCT_INFO` VARCHAR(512) NULL DEFAULT NULL COMMENT '扣费信息(JSON):存件点/取件点/司机扣费明细' AFTER `BEFORE_STATUS`;
-- ============================================================
-- 2026/05/15 è®¢å•表增加是否手动退款字段
-- ============================================================
ALTER TABLE `orders` ADD COLUMN `MANUAL_REFUND` INT DEFAULT 0 COMMENT '是否手动退款:0=否;1=是' AFTER `DEDUCTION_AMOUNT`;
-- ============================================================
-- 2026/05/14 è®¢å•表增加优惠券字段
-- ============================================================
ALTER TABLE `orders` ADD COLUMN `COUPON_ID` INT DEFAULT NULL COMMENT '优惠券主键' AFTER `OUT_TRADE_NO`;
ALTER TABLE `orders` ADD COLUMN `DEDUCTION_AMOUNT` BIGINT DEFAULT NULL COMMENT '抵扣金额(分)' AFTER `COUPON_ID`;
-- ============================================================
-- 2026/05/14 ä¼šå‘˜è¡¨å¢žåŠ é‚®ç®±å­—æ®µ
-- ============================================================
ALTER TABLE `member` ADD COLUMN `EMAIL` VARCHAR(128) DEFAULT NULL COMMENT '邮箱' AFTER `NAME`;
-- ============================================================
-- 2026/05/13 æ³¨å†Œæ»¡å¹´èµ é€ä¼˜æƒ åˆ¸å®šæ—¶ä»»åŠ¡
server/services/src/main/java/com/doumee/biz/system/impl/OperationConfigBizImpl.java
@@ -9,6 +9,9 @@
import com.doumee.dao.system.model.SystemDictData;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.math.BigDecimal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -46,6 +49,11 @@
        dto.setRegisterCouponGiftCount(getValue(Constants.OP_REGISTER_COUPON_GIFT_COUNT));
        dto.setRegisterCouponId(getValue(Constants.OP_REGISTER_COUPON_ID));
        dto.setRegisterGiftCouponIds(getValue(Constants.OP_REGISTER_GIFT_COUPON_IDS));
        dto.setRegisterRewardOrderCount(getValue(Constants.OP_REGISTER_REWARD_ORDER_COUNT));
        dto.setRegisterRewardAmount(fenToYuan(getValue(Constants.OP_REGISTER_REWARD_AMOUNT)));
        dto.setPlatformRewardOrderCount(getValue(Constants.OP_PLATFORM_REWARD_ORDER_COUNT));
        dto.setPlatformRewardAmount(fenToYuan(getValue(Constants.OP_PLATFORM_REWARD_AMOUNT)));
        dto.setInvoiceMonthLimit(getValue(Constants.OP_INVOICE_MONTH_LIMIT));
        return dto;
    }
@@ -73,6 +81,11 @@
        saveOrUpdate(Constants.OP_REGISTER_COUPON_GIFT_COUNT, "注册赠送-至多赠送次数", dto.getRegisterCouponGiftCount());
        saveOrUpdate(Constants.OP_REGISTER_COUPON_ID, "注册赠送-优惠券ID", dto.getRegisterCouponId());
        saveOrUpdate(Constants.OP_REGISTER_GIFT_COUPON_IDS, "新注册赠送-优惠券IDs", dto.getRegisterGiftCouponIds());
        saveOrUpdate(Constants.OP_REGISTER_REWARD_ORDER_COUNT, "注册完成订单奖励单数", dto.getRegisterRewardOrderCount());
        saveOrUpdate(Constants.OP_REGISTER_REWARD_AMOUNT, "注册完成订单奖励金额(分)", yuanToFen(dto.getRegisterRewardAmount()));
        saveOrUpdate(Constants.OP_PLATFORM_REWARD_ORDER_COUNT, "平台完成订单奖励单数", dto.getPlatformRewardOrderCount());
        saveOrUpdate(Constants.OP_PLATFORM_REWARD_AMOUNT, "平台完成订单奖励金额(分)", yuanToFen(dto.getPlatformRewardAmount()));
        saveOrUpdate(Constants.OP_INVOICE_MONTH_LIMIT, "发票开具月份限制(月)", dto.getInvoiceMonthLimit());
    }
    private String getValue(String label) {
@@ -97,6 +110,20 @@
        }
    }
    private String fenToYuan(String fen) {
        if (StringUtils.isBlank(fen)) {
            return null;
        }
        return new BigDecimal(fen).divide(new BigDecimal("100"), 2, BigDecimal.ROUND_HALF_UP).toPlainString();
    }
    private String yuanToFen(String yuan) {
        if (StringUtils.isBlank(yuan)) {
            return null;
        }
        return new BigDecimal(yuan).multiply(new BigDecimal("100")).setScale(0, BigDecimal.ROUND_HALF_UP).toPlainString();
    }
    private void validate(OperationConfigDTO dto) {
        if (dto == null
                || StringUtils.isBlank(dto.getDriverDailyCancelLimit())
server/services/src/main/java/com/doumee/core/constants/Constants.java
@@ -115,6 +115,11 @@
    public static final String OP_REGISTER_COUPON_GIFT_COUNT = "REGISTER_COUPON_GIFT_COUNT";
    public static final String OP_REGISTER_COUPON_ID = "REGISTER_COUPON_ID";
    public static final String OP_REGISTER_GIFT_COUPON_IDS = "REGISTER_GIFT_COUPON_IDS";
    public static final String OP_REGISTER_REWARD_ORDER_COUNT = "REGISTER_REWARD_ORDER_COUNT";
    public static final String OP_REGISTER_REWARD_AMOUNT = "REGISTER_REWARD_AMOUNT";
    public static final String OP_PLATFORM_REWARD_ORDER_COUNT = "PLATFORM_REWARD_ORDER_COUNT";
    public static final String OP_PLATFORM_REWARD_AMOUNT = "PLATFORM_REWARD_AMOUNT";
    public static final String OP_INVOICE_MONTH_LIMIT = "INVOICE_MONTH_LIMIT";
    // èŠ¯çƒ¨äº‘æ‰“å°æœºé…ç½®
    public static final String XPYUN_CONFIG = "XPYUN_CONFIG";
@@ -613,7 +618,8 @@
        EVALUATED("evaluated", "订单已评价", "行李订单:{orderNo}用户已完成评价,可前往订单查看评价内容"),
        REFUNDING("refunding", "订单退款中", "行李订单:{orderNo}用户已提交退款申请,该订单任务已取消,请勿前往。"),
        SETTLED("settled", "订单已结算", "行李订单:{orderNo}平台已完成结算,金额为{amount}元,请注意查收。"),
        CANCELLED("cancelled", "订单取消成功", "行李订单:{orderNo}已帮您取消,您今日还可主动取消{cancelLimit}次订单,请合理安排接单。")
        CANCELLED("cancelled", "订单取消成功", "行李订单:{orderNo}已帮您取消,您今日还可主动取消{cancelLimit}次订单,请合理安排接单。"),
        REWARD_UNLOCKED("rewardUnlocked", "平台奖励已解锁", "尊敬的司机,您的平台奖励已解锁,完成更多订单奖励更丰盛,点击前往【奖励大厅】查看吧!"),
        ;
        private final String key;
@@ -658,6 +664,8 @@
        SHOP_AUTH_APPROVED_DEPOSIT("SMS_506135030", "门店端-审核通过需缴纳押金", "恭喜您!您的门店\"{storeName}\"已通过初步审核。请支付押金{money}元以完成入驻,支付后即可登录门店后台正式接单。", true),
        SHOP_AUTH_SUCCESS("SMS_505885083", "门店端-成功入驻通知", "恭喜您!您的门店\"{storeName}\"已通过平台审核,正式入驻成功。 æ‚¨å¯ç™»å½•商家后台开始接单,账号:注册手机号,初始密码::{password}(建议首次登录后修改)。", true),
        MEMBER_TIME_OUT("SMS_506190182", "会员端-即将超时", "您的行李订单:{orderNo}即将到达预计取件时间,请尽快取件,超时将产生逾期费用,请知悉。", true),
        EXCEPTION_DISPATCH("SMS_506110517", "司机端-异常派单", "您有异常派单需处理:订单号{orderNo},请将行李送至新寄存点({address}),存件码{code}。送达后交由门店核验,完成转运任务。",true),
        MEMBER_EXCEPTION_DISPATCH("SMS_506115543", "会员端-异常派单", "尊敬的客户,因多次联系不上您,为确保行李安全,已将其转运至新寄存点:{address}。请尽快前往取件,相关费用将由您承担。如有疑问请联系客服,感谢您的理解与支持!",true),
        ;
        private final String templateCode;
server/services/src/main/java/com/doumee/dao/business/InvoiceRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.doumee.dao.business;
import com.doumee.dao.business.model.InvoiceRecord;
import com.github.yulichang.base.MPJBaseMapper;
/**
 * å‘票申请记录Mapper
 *
 * @author rk
 * @date 2026/05/18
 */
public interface InvoiceRecordMapper extends MPJBaseMapper<InvoiceRecord> {
}
server/services/src/main/java/com/doumee/dao/business/RewardRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.doumee.dao.business;
import com.doumee.dao.business.model.RewardRecord;
import com.github.yulichang.base.MPJBaseMapper;
/**
 * å¥–励记录Mapper
 *
 * @author rk
 * @date 2026/05/15
 */
public interface RewardRecordMapper extends MPJBaseMapper<RewardRecord> {
}
server/services/src/main/java/com/doumee/dao/business/model/Category.java
@@ -63,8 +63,8 @@
    @ExcelColumn(name="单位名称")
    private String name;
    @ApiModelProperty(value = "类型:1=车辆类型;2=物品分类;3=物品等级;4=物品尺寸;", example = "1")
    @ExcelColumn(name="类型:1=车辆类型;2=物品分类;3=物品等级;4=物品尺寸;")
    @ApiModelProperty(value = "类型:1=车辆类型;2=物品分类;3=物品等级;4=物品尺寸;5=位置标签;", example = "1")
    @ExcelColumn(name="类型:1=车辆类型;2=物品分类;3=物品等级;4=物品尺寸;5=位置标签;")
    private Integer type;
    @ApiModelProperty(value = "内容 ï¼ˆtype=1:通行方式 æœºåŠ¨è½¦ = driving éžæœºåŠ¨è½¦ = bicycling ; type=2=寄存说明 ï¼› type=3:司机评级;type=4:是否特大尺寸 0=否;1=是;);")
server/services/src/main/java/com/doumee/dao/business/model/DriverInfo.java
@@ -57,9 +57,9 @@
    @ExcelColumn(name = "手机号", index = 3, width = 12)
    private String telephone;
    @ApiModelProperty(value = "性别:1=男;2=女", example = "1")
    @ApiModelProperty(value = "性别:1=男;2=女;", example = "1")
    @TableField(exist = false)
    @ExcelColumn(name = "性别", index = 2, width = 6, valueMapping = "1=男;2=女")
    @ExcelColumn(name = "性别", index = 2, width = 6, valueMapping = "1=男;2=女")
    private Integer gender;
    @ApiModelProperty(value = "身份证号码")
@@ -97,11 +97,11 @@
    private Date cardEndDate;
    @ApiModelProperty(value = "司机状态:0=启用;1=禁用;", example = "0")
    @ExcelColumn(name = "状态", index = 8, width = 8, valueMapping = "0=启用;1=禁用;")
    @ExcelColumn(name = "状态", index = 8, width = 8, valueMapping = "0=启用;1=禁用;")
    private Integer status;
    @ApiModelProperty(value = "审批状态:0=待审批;1=审批通过;2=审批驳回;3=已支付押金;99=未认证;", example = "0")
    @ExcelColumn(name = "审批状态", index = 9, width = 10, valueMapping = "审批状态:0=待审批;1=审批通过;2=审批驳回;3=已支付押金;99=未认证;")
    @ExcelColumn(name = "审批状态", index = 9, width = 10, valueMapping = "0=待审批;1=审批通过;2=审批驳回;3=已支付押金;99=未认证;")
    private Integer auditStatus;
    @ApiModelProperty(value = "OPENID(APP)")
server/services/src/main/java/com/doumee/dao/business/model/InvoiceRecord.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
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.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
 * å‘票申请记录
 *
 * @author rk
 * @date 2026/05/18
 */
@Data
@ApiModel("发票申请记录")
@TableName("`invoice_record`")
public class InvoiceRecord {
    @TableId(type = IdType.AUTO)
    @ApiModelProperty(value = "主键", example = "1")
    private Integer id;
    @ApiModelProperty(value = "是否已删除 0未删除 1已删除", example = "0")
    private Integer deleted;
    @ApiModelProperty(value = "创建人编码", example = "1")
    private Integer createUser;
    @ApiModelProperty(value = "创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    @ApiModelProperty(value = "更新人编码", example = "1")
    private Integer updateUser;
    @ApiModelProperty(value = "更新时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;
    @ApiModelProperty(value = "备注")
    private String remark;
    @ApiModelProperty(value = "订单主键")
    private Integer orderId;
    @ApiModelProperty(value = "会员主键")
    private Integer memberId;
    @ApiModelProperty(value = "订单编号")
    private String orderNo;
    @ApiModelProperty(value = "发票号码")
    private String invoiceNo;
    @ApiModelProperty(value = "组织类型:0=个人/事业单位;1=企业")
    private Integer orgType;
    @ApiModelProperty(value = "发票类型:0=电子普通发票;1=电子专用发票")
    private Integer invoiceType;
    @ApiModelProperty(value = "发票抬头")
    private String name;
    @ApiModelProperty(value = "税号")
    private String taxId;
    @ApiModelProperty(value = "开户银行")
    private String bankName;
    @ApiModelProperty(value = "银行账号")
    private String bankAccount;
    @ApiModelProperty(value = "企业地址(ORG_TYPE=1)")
    private String companyAddr;
    @ApiModelProperty(value = "企业电话(ORG_TYPE=1)")
    private String companyPhone;
    @ApiModelProperty(value = "发票金额(分)")
    private Long invoiceAmount;
    @ApiModelProperty(value = "申请状态:0=申请中;1=开具成功;99=开具失败")
    private Integer status;
    @ApiModelProperty(value = "发票文件地址")
    private String fileAddr;
    @ApiModelProperty(value = "开票邮箱")
    private String email;
    @ApiModelProperty(value = "发票PDF文件KEY")
    private String invoicePdfKey;
    @TableField(exist = false)
    @ApiModelProperty(value = "申请开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date startDate;
    @TableField(exist = false)
    @ApiModelProperty(value = "申请结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
}
server/services/src/main/java/com/doumee/dao/business/model/Member.java
@@ -67,6 +67,9 @@
    @ExcelColumn(name="真实姓名",index = 4,width = 10)
    private String name;
    @ApiModelProperty(value = "邮箱")
    private String email;
    @ApiModelProperty(value = "当前余额(单位:分)", example = "1")
    private Long amount;
server/services/src/main/java/com/doumee/dao/business/model/Notice.java
@@ -59,7 +59,7 @@
    @ApiModelProperty(value = "对象编码", example = "1")
    private Integer objId;
    @ApiModelProperty(value = "对象类型:0=订单类型;99=其他类型", example = "0")
    @ApiModelProperty(value = "对象类型:0=订单;2=奖励大厅;99=其他", example = "0")
    private Integer objType;
    @ApiModelProperty(value = "状态:0=未读;1=已读", example = "0")
server/services/src/main/java/com/doumee/dao/business/model/Orders.java
@@ -213,7 +213,7 @@
    @ExcelColumn(name = "预估费用")
    private Long estimatedAmount;
    @ApiModelProperty(value = "基础费用/单价(分)")
    @ApiModelProperty(value = "基础费用-单价(分)")
    @ExcelColumn(name = "基础费用")
    private Long price;
@@ -247,7 +247,7 @@
    @ExcelColumn(name = "基础寄存费用")
    private Long basicAmount;
    @ApiModelProperty(value = "订单总费用(分)")
    @ApiModelProperty(value = "订单总费用(分)",notes = "计入逾期费用与优惠券费用")
    @ExcelColumn(name = "订单总费用")
    private Long totalAmount;
@@ -364,6 +364,21 @@
    @ApiModelProperty(value = "三方订单号")
    private String outTradeNo;
    @ApiModelProperty(value = "优惠券主键")
    private Integer couponId;
    @ApiModelProperty(value = "优惠券抵扣金额(分)")
    private Long deductionAmount;
    @ApiModelProperty(value = "是否手动退款:0=否;1=是", example = "0")
    private Integer manualRefund;
    @ApiModelProperty(value = "发票状态:0=不可申请;1=可申请;2=申请中;3=开具成功;99=开具失败", example = "0")
    private Integer invoiceStatus;
    @ApiModelProperty(value = "门店异常补偿金额(分)")
    private Long shopCompensationAmount;
    @ApiModelProperty(value = "无人接单是否已短信通知平台:0=否;1=是", example = "0")
    private Integer platformSmsNotified;
server/services/src/main/java/com/doumee/dao/business/model/OrdersRefund.java
@@ -48,7 +48,7 @@
    @ApiModelProperty(value = "订单主键", example = "1")
    private Integer orderId;
    @ApiModelProperty(value = "退款方式:0=未存件直接取消;1=平台直接取消;2=已存件申请取消;3=订单完成退款", example = "0")
    @ApiModelProperty(value = "退款方式:0=未存件直接取消;1=平台直接取消;2=已存件申请取消;3=订单完成退款;4=手动退款", example = "0")
    private Integer type;
    @ApiModelProperty(value = "取消原因")
@@ -76,6 +76,9 @@
    @ApiModelProperty(value = "退款前订单状态", example = "1")
    private Integer beforeStatus;
    @ApiModelProperty(value = "扣费信息(JSON)", notes = "{\"depositShopDeduct\":100,\"takeShopDeduct\":50,\"driverDeduct\":200},手动退款时使用")
    private String deductInfo;
    @TableField(exist = false)
    @ApiModelProperty(value = "订单编号(关联查询)")
    private String orderCode;
server/services/src/main/java/com/doumee/dao/business/model/Revenue.java
@@ -57,8 +57,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 = 4, width = 12)
    @ApiModelProperty(value = "类型:0=完成订单;1=提现支出;2=提现退回;3=平台奖励;4=责任扣款;5=异常金额", example = "0")
    @ExcelColumn(name = "业务类型", valueMapping = "0=完成订单;1=提现支出;2=提现退回;3=平台奖励;4=责任扣款;5=异常金额;", index = 4, width = 12)
    private Integer type;
    @ApiModelProperty(value = "收支类型:1=收入;-1=支出", example = "1")
server/services/src/main/java/com/doumee/dao/business/model/RewardRecord.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,69 @@
package com.doumee.dao.business.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
 * å¥–励记录
 *
 * @author rk
 * @date 2026/05/15
 */
@Data
@ApiModel("奖励记录")
@TableName("`reward_record`")
public class RewardRecord {
    @TableId(type = IdType.AUTO)
    @ApiModelProperty(value = "主键", example = "1")
    private Integer id;
    @ApiModelProperty(value = "是否已删除 0未删除 1已删除", example = "0")
    private Integer deleted;
    @ApiModelProperty(value = "创建人编码", example = "1")
    private Integer createUser;
    @ApiModelProperty(value = "创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    @ApiModelProperty(value = "更新人编码", example = "1")
    private Integer updateUser;
    @ApiModelProperty(value = "更新时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;
    @ApiModelProperty(value = "备注")
    private String remark;
    @ApiModelProperty(value = "订单主键")
    private Integer orderId;
    @ApiModelProperty(value = "司机主键")
    private Integer driverId;
    @ApiModelProperty(value = "状态:0=待领取;1=已领取")
    private Integer status;
    @ApiModelProperty(value = "奖励类型:0=注册奖励;1=门店奖励")
    private Integer type;
    @ApiModelProperty(value = "领取时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date claimTime;
    @ApiModelProperty(value = "领取收支记录主键")
    private Integer revenueId;
    @ApiModelProperty(value = "奖励金额(分)")
    private Long amount;
}
server/services/src/main/java/com/doumee/dao/business/model/ShopInfo.java
@@ -180,6 +180,9 @@
    @ApiModelProperty(value = "寄存类型(逗号分隔的category主键)")
    private String depositTypes;
    @ApiModelProperty(value = "位置标签主键(关联category type=5,多个以逗号分割)")
    private String locationTagIds;
    @ApiModelProperty(value = "收费标准")
    private String feeStandard;
server/services/src/main/java/com/doumee/dao/dto/ApplyInvoiceDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package com.doumee.dao.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
 * å¼€ç¥¨ç”³è¯·è¯·æ±‚
 *
 * @author rk
 * @date 2026/05/18
 */
@Data
@ApiModel("开票申请请求")
public class ApplyInvoiceDTO implements Serializable {
    @NotNull(message = "订单主键不能为空")
    @ApiModelProperty(value = "订单主键", required = true)
    private Integer orderId;
    @NotNull(message = "组织类型不能为空")
    @ApiModelProperty(value = "组织类型:0=个人/事业单位;1=企业", required = true)
    private Integer orgType;
    @NotNull(message = "发票类型不能为空")
    @ApiModelProperty(value = "发票类型:0=电子普通发票;1=电子专用发票", required = true)
    private Integer invoiceType;
    @NotBlank(message = "发票抬头不能为空")
    @ApiModelProperty(value = "发票抬头", required = true)
    private String name;
    @ApiModelProperty(value = "税号(企业必填)")
    private String taxId;
    @ApiModelProperty(value = "开户银行(专用发票必填)")
    private String bankName;
    @ApiModelProperty(value = "银行账号(专用发票必填)")
    private String bankAccount;
    @ApiModelProperty(value = "企业地址(专用发票必填)")
    private String companyAddr;
    @ApiModelProperty(value = "企业电话(专用发票必填)")
    private String companyPhone;
}
server/services/src/main/java/com/doumee/dao/dto/AuditDTO.java
@@ -1,6 +1,7 @@
package com.doumee.dao.dto;
import io.swagger.annotations.ApiModel;
import java.math.BigDecimal;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -26,6 +27,15 @@
    @ApiModelProperty(value = "司机定级:5=S;4=A;3=B;2=C;1=D(审批通过时必填)", example = "5")
    private Integer driverLevel;
    @ApiModelProperty(value = "就地寄存收益比例(审批通过时必填)", example = "5")
    private BigDecimal localDeposit;
    @ApiModelProperty(value = "异地寄存收益比例(审批通过时必填)", example = "5")
    private BigDecimal remoteDeposit;
    @ApiModelProperty(value = "异地取件收益比例(审批通过时必填)", example = "5")
    private BigDecimal remoteTake;
    @ApiModelProperty(value = "审批人编码(服务端填充)", hidden = true)
    private Integer auditUser;
server/services/src/main/java/com/doumee/dao/dto/CalculateLocalPriceDTO.java
@@ -40,4 +40,10 @@
    @ApiModelProperty(value = "保价金额(元)")
    private BigDecimal declaredAmount;
    @ApiModelProperty(value = "会员主键", hidden = true)
    private Integer memberId;
    @ApiModelProperty(value = "优惠券主键:-1=自动选择最优;null=不使用;>0=指定优惠券ID")
    private Integer couponId;
}
server/services/src/main/java/com/doumee/dao/dto/CalculateRemotePriceDTO.java
@@ -50,4 +50,10 @@
    @ApiModelProperty(value = "是否加急")
    private Boolean urgent;
    @ApiModelProperty(value = "会员主键", hidden = true)
    private Integer memberId;
    @ApiModelProperty(value = "优惠券主键:-1=自动选择最优;null=不使用;>0=指定优惠券ID")
    private Integer couponId;
}
server/services/src/main/java/com/doumee/dao/dto/ClaimRewardDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package com.doumee.dao.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
 * é¢†å–奖励金请求
 *
 * @Author : Rk
 * @create 2026/05/15
 */
@Data
@ApiModel("领取奖励金请求")
public class ClaimRewardDTO implements Serializable {
    @NotNull(message = "奖励记录ID不能为空")
    @ApiModelProperty(value = "奖励记录ID", required = true)
    private Integer rewardRecordId;
}
server/services/src/main/java/com/doumee/dao/dto/CreateOrderDTO.java
@@ -82,4 +82,7 @@
    @ApiModelProperty(value = "订单备注(非必填)")
    private String remark;
    @ApiModelProperty(value = "优惠券主键(可选)")
    private Integer couponId;
}
server/services/src/main/java/com/doumee/dao/dto/DataBoardQueryDTO.java
@@ -1,25 +1,70 @@
package com.doumee.dao.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;
@Data
@ApiModel("数据看板查询条件")
public class DataBoardQueryDTO implements Serializable {
    @ApiModelProperty(value = "开始时间")
    @ApiModelProperty(value = "日期段类型:0=今日,1=近七天,2=近30天,3=近半年,4=近一年", required = true)
    private Integer dateType;
    @JsonIgnore
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date startDate;
    @ApiModelProperty(value = "结束时间")
    @JsonIgnore
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
    @ApiModelProperty(value = "门店主键(可选)")
    private Integer shopId;
    public void resolveDateRange() {
        Calendar now = Calendar.getInstance();
        now.set(Calendar.MINUTE, 0);
        now.set(Calendar.SECOND, 0);
        now.set(Calendar.MILLISECOND, 0);
        now.set(Calendar.HOUR_OF_DAY, 0);
        Calendar start = Calendar.getInstance();
        start.set(Calendar.MINUTE, 0);
        start.set(Calendar.SECOND, 0);
        start.set(Calendar.MILLISECOND, 0);
        start.set(Calendar.HOUR_OF_DAY, 0);
        switch (dateType != null ? dateType : 0) {
            case 1:
                start.add(Calendar.DAY_OF_MONTH, -6);
                break;
            case 2:
                start.add(Calendar.DAY_OF_MONTH, -29);
                break;
            case 3:
                start.add(Calendar.MONTH, -6);
                break;
            case 4:
                start.add(Calendar.YEAR, -1);
                break;
            default:
                // 0=今日,start å·²ç»æ˜¯ä»Šå¤©
                break;
        }
        this.startDate = start.getTime();
        Calendar end = Calendar.getInstance();
        end.set(Calendar.HOUR_OF_DAY, 23);
        end.set(Calendar.MINUTE, 59);
        end.set(Calendar.SECOND, 59);
        end.set(Calendar.MILLISECOND, 999);
        this.endDate = end.getTime();
    }
}
server/services/src/main/java/com/doumee/dao/dto/FinanceQueryDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package com.doumee.dao.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@ApiModel("平台财务查询条件")
public class FinanceQueryDTO implements Serializable {
    @ApiModelProperty(value = "开始年月", required = true)
    @JsonFormat(pattern = "yyyy-MM")
    private Date startDate;
    @ApiModelProperty(value = "结束年月", required = true)
    @JsonFormat(pattern = "yyyy-MM")
    private Date endDate;
}
server/services/src/main/java/com/doumee/dao/dto/HandleOrderExceptionDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package com.doumee.dao.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
 * è®¢å•异常处理请求
 *
 * @author rk
 * @date 2026/05/18
 */
@Data
@ApiModel("订单异常处理请求")
public class HandleOrderExceptionDTO implements Serializable {
    @NotNull(message = "订单主键不能为空")
    @ApiModelProperty(value = "操作订单主键", required = true)
    private Integer orderId;
    @NotNull(message = "存放门店主键不能为空")
    @ApiModelProperty(value = "存放门店主键", required = true)
    private Integer depositShopId;
    @NotNull(message = "异常补偿司机金额不能为空")
    @ApiModelProperty(value = "异常补偿司机金额(分)", required = true)
    private Long driverCompensation;
    @NotNull(message = "异常补偿门店金额不能为空")
    @ApiModelProperty(value = "异常补偿门店金额(分)", required = true)
    private Long shopCompensation;
    @NotBlank(message = "异常备注不能为空")
    @ApiModelProperty(value = "异常备注", required = true)
    private String remark;
}
server/services/src/main/java/com/doumee/dao/dto/ManualRefundDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package com.doumee.dao.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
 * æ‰‹åŠ¨é€€æ¬¾è¯·æ±‚DTO
 *
 * @Author : Rk
 * @create 2026/05/15
 */
@Data
@ApiModel("手动退款请求")
public class ManualRefundDTO {
    @ApiModelProperty(value = "订单主键", example = "1", required = true)
    @NotNull(message = "订单主键不能为空")
    private Integer orderId;
    @ApiModelProperty(value = "退款金额(分)", example = "10000", required = true,
            notes = "必须大于0且小于等于(支付金额-已退款金额)")
    @NotNull(message = "退款金额不能为空")
    private Long refundAmount;
    @ApiModelProperty(value = "存件门店扣款金额(分)", example = "500",
            notes = "所有订单类型均存在存件门店,非必填,不填则不扣款")
    private Long depositShopDeduct;
    @ApiModelProperty(value = "取件门店扣款金额(分)", example = "300",
            notes = "仅异地订单(type=1)且存在取件门店(takeShopId!=null)时有效,其他情况传入忽略")
    private Long takeShopDeduct;
    @ApiModelProperty(value = "司机扣款金额(分)", example = "200",
            notes = "仅异地订单(type=1)时有效,其他情况传入忽略")
    private Long driverDeduct;
    @ApiModelProperty(value = "备注")
    private String remark;
}
server/services/src/main/java/com/doumee/dao/dto/MyOrderDTO.java
@@ -24,4 +24,7 @@
    @ApiModelProperty(value = "搜索关键词(收件人/收件人电话模糊/订单号精准)")
    private String keyword;
    @ApiModelProperty(value = "发票状态筛选(可选):1=可开票订单", example = "1")
    private Integer invoiceStatus;
}
server/services/src/main/java/com/doumee/dao/dto/OperationConfigDTO.java
@@ -73,4 +73,19 @@
    @ApiModelProperty(value = "新注册用户赠送优惠券ID(多个以,分割)")
    private String registerGiftCouponIds;
    @ApiModelProperty(value = "注册完成订单奖励单数")
    private String registerRewardOrderCount;
    @ApiModelProperty(value = "注册完成订单奖励金额(分)")
    private String registerRewardAmount;
    @ApiModelProperty(value = "平台完成订单奖励单数")
    private String platformRewardOrderCount;
    @ApiModelProperty(value = "平台完成订单奖励金额(分)")
    private String platformRewardAmount;
    @ApiModelProperty(value = "发票开具月份限制(月)", required = true)
    private String invoiceMonthLimit;
}
server/services/src/main/java/com/doumee/dao/dto/RevenueQueryDTO.java
@@ -14,7 +14,7 @@
    @ApiModelProperty(value = "收支类型:1=收入;-1=支出")
    private Integer optType;
    @ApiModelProperty(value = "变动类型:0=完成订单;1=提现支出;2=提现退回;3=平台奖励")
    @ApiModelProperty(value = "变动类型:0=完成订单;1=提现支出;2=提现退回;3=平台奖励;4=责任扣款")
    private Integer type;
    @JsonFormat(pattern = "yyyy-MM-dd")
server/services/src/main/java/com/doumee/dao/dto/SendInvoiceEmailDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package com.doumee.dao.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
 * å‘送发票邮件请求
 *
 * @author rk
 * @date 2026/05/18
 */
@Data
@ApiModel("发送发票邮件请求")
public class SendInvoiceEmailDTO implements Serializable {
    @NotNull(message = "发票记录ID不能为空")
    @ApiModelProperty(value = "发票记录ID", required = true)
    private Integer invoiceRecordId;
    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    @ApiModelProperty(value = "接收邮箱", required = true)
    private String email;
}
server/services/src/main/java/com/doumee/dao/dto/ShopNearbyDTO.java
@@ -39,4 +39,7 @@
    @ApiModelProperty(value = "城市主键", example = "1")
    private Integer cityId;
    @ApiModelProperty(value = "位置标签主键(category type=5)", example = "1")
    private Integer locationTagId;
}
server/services/src/main/java/com/doumee/dao/dto/ShopRevenueQueryDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.doumee.dao.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.Date;
/**
 * é—¨åº—营收查询请求
 *
 * @Author : Rk
 * @create 2026/05/15
 */
@Data
@ApiModel("门店营收查询请求")
public class ShopRevenueQueryDTO implements Serializable {
    @ApiModelProperty(value = "开始日期", example = "2026-01-01")
    @NotNull(message = "开始日期不能为空")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date startDate;
    @ApiModelProperty(value = "结束日期", example = "2026-12-31")
    @NotNull(message = "结束日期不能为空")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date endDate;
}
server/services/src/main/java/com/doumee/dao/dto/ShopUpdateDTO.java
@@ -130,4 +130,8 @@
    @NotNull(message = "异地取件分成占比不能为空")
    private Double remoteTake;
    @ApiModelProperty(value = "位置标签主键(关联category type=5,多个以逗号分割)", required = true)
    @NotBlank(message = "位置标签不能为空")
    private String locationTagIds;
}
server/services/src/main/java/com/doumee/dao/dto/TrendQueryDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package com.doumee.dao.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel("趋势查询条件")
public class TrendQueryDTO implements Serializable {
    @ApiModelProperty(value = "查询年月,格式yyyy-MM(按月查询)")
    private String month;
    @ApiModelProperty(value = "查询年份,格式yyyy(按年查询)")
    private String year;
}
server/services/src/main/java/com/doumee/dao/dto/invoice/InvoiceA0001DTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,105 @@
package com.doumee.dao.dto.invoice;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel("A0001开具蓝字发票数据")
public class InvoiceA0001DTO {
    @ApiModelProperty("【必填】固定参数 API")
    private String data_resources;
    @ApiModelProperty("发票类型:026-电子普通发票 81-全电专票 82-全电普票")
    private String itype;
    @ApiModelProperty("【必填】销售方纳税人识别号")
    private String nsrsbh;
    @ApiModelProperty("税控盘号(使用税控盒子必填,其他设备为空)")
    private String skph;
    @ApiModelProperty("【必填】业务单据号,必须唯一")
    private String order_num;
    @ApiModelProperty("【必填】税收编码版本号,如29.0,具体值请询问提供商")
    private String bmb_bbh;
    @ApiModelProperty("【必填】征税方式 0-普通征税 1-减按计增 2-差额征税")
    private String zsfs;
    @ApiModelProperty("特殊票种标识 00-正常票种 01-农产品销售 02-农产品收购")
    private String tspz;
    @ApiModelProperty("【必填】销售方纳税人识别号")
    private String xsf_nsrsbh;
    @ApiModelProperty("【必填】销售方名称")
    private String xsf_mc;
    @ApiModelProperty("【必填】销售方地址、电话")
    private String xsf_dzdh;
    @ApiModelProperty("【必填】销售方开户行名称与银行账号")
    private String xsf_yhzh;
    @ApiModelProperty("购买方纳税人识别号(企业用户为必填项)")
    private String gmf_nsrsbh;
    @ApiModelProperty("【必填】购买方名称")
    private String gmf_mc;
    @ApiModelProperty("购买方地址、电话")
    private String gmf_dzdh;
    @ApiModelProperty("购买方开户行名称与银行账号")
    private String gmf_yhzh;
    @ApiModelProperty("【必填】开票人")
    private String kpr;
    @ApiModelProperty("收款人")
    private String skr;
    @ApiModelProperty("复核人")
    private String fhr;
    @ApiModelProperty("原发票代码")
    private String yfp_dm;
    @ApiModelProperty("原发票号码")
    private String yfp_hm;
    @ApiModelProperty("【必填】价税合计(元,2位小数)= åˆè®¡é‡‘额(不含税) + åˆè®¡ç¨Žé¢")
    private String jshj;
    @ApiModelProperty("【必填】合计金额(不含税,元,2位小数)")
    private String hjje;
    @ApiModelProperty("【必填】合计税额(元,2位小数)")
    private String hjse;
    @ApiModelProperty("扣除额(2位小数),当zsfs为2时必填")
    private String kce;
    @ApiModelProperty("备注(最长100字符)")
    private String bz;
    @ApiModelProperty("已失效,不再支持")
    private String kpzdbs;
    @ApiModelProperty("手机号,税控盒子主动交付时填写")
    private String jff_phone;
    @ApiModelProperty("电子邮件,税控盒子主动交付时填写")
    private String jff_email;
    @ApiModelProperty("适用3%征收率减按1%政策时的原因标识:2-前期已开票需红冲重开 3-放弃减按1%政策")
    private String sslkjly;
    @ApiModelProperty("【必填】商品明细列表")
    private List<InvoiceA0001ItemDTO> common_fpkj_xmxx;
}
server/services/src/main/java/com/doumee/dao/dto/invoice/InvoiceA0001ItemDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package com.doumee.dao.dto.invoice;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("A0001商品明细")
public class InvoiceA0001ItemDTO {
    @ApiModelProperty("【必填】发票行性质 0-正常行 1-折扣行 2-被折扣行")
    private String fphxz;
    @ApiModelProperty("【必填】商品编码(税务总局19位税控编码)")
    private String spbm;
    @ApiModelProperty("自行编码(一般不建议使用)")
    private String zxbm;
    @ApiModelProperty("优惠政策标识 0-不使用 1-使用")
    private String yhzcbs;
    @ApiModelProperty("零税率标识 ç©º-非零税率 1-免税 2-不征收 3-普通零税率")
    private String lslbs;
    @ApiModelProperty("增值税特殊管理(yhzcbs为1时必填,值为中文)")
    private String zzstsgl;
    @ApiModelProperty("【必填】项目名称(须与商品编码表一致)")
    private String xmmc;
    @ApiModelProperty("规格型号(折扣行不传)")
    private String ggxh;
    @ApiModelProperty("计量单位(折扣行不传)")
    private String dw;
    @ApiModelProperty("项目数量(小数点后6位,大于0)")
    private String xmsl;
    @ApiModelProperty("项目单价(含税单价,小数点后6位,大于0)")
    private String xmdj;
    @ApiModelProperty("【必填】项目金额(含税,元,2位小数)")
    private String xmje;
    @ApiModelProperty("【必填】税率(例1%为0.01)")
    private String sl;
    @ApiModelProperty("【必填】税额(元,2位小数)")
    private String se;
}
server/services/src/main/java/com/doumee/dao/dto/invoice/InvoiceA0001Request.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.doumee.dao.dto.invoice;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("A0001开具蓝字发票请求")
public class InvoiceA0001Request {
    @ApiModelProperty("access_token")
    private String access_token;
    @ApiModelProperty("serviceKey")
    private String serviceKey;
    @ApiModelProperty("开票数据")
    private InvoiceA0001DTO data;
}
server/services/src/main/java/com/doumee/dao/dto/invoice/InvoiceA0001Result.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.doumee.dao.dto.invoice;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel("A0001开具蓝字发票响应")
public class InvoiceA0001Result {
    @ApiModelProperty("消息")
    private String msg;
    @ApiModelProperty("结果 SUCCESS/FAIL")
    private String result;
    @ApiModelProperty("发票列表")
    private List<InvoiceA0001ResultItem> rows;
    @ApiModelProperty("总数")
    private String total;
}
server/services/src/main/java/com/doumee/dao/dto/invoice/InvoiceA0001ResultItem.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
package com.doumee.dao.dto.invoice;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel("A0001开具蓝字发票结果项")
public class InvoiceA0001ResultItem {
    @ApiModelProperty("复核人")
    private String fhr;
    @ApiModelProperty("销售方名称")
    private String xsf_mc;
    @ApiModelProperty("原发票号码")
    private String yfp_hm;
    @ApiModelProperty("备注")
    private String bz;
    @ApiModelProperty("销售方银行账号")
    private String xsf_yhzh;
    @ApiModelProperty("购买方名称")
    private String gmf_mc;
    @ApiModelProperty("合计税额")
    private String hjse;
    @ApiModelProperty("发票代码")
    private String fp_dm;
    @ApiModelProperty("扣除额")
    private String kce;
    @ApiModelProperty("原发票代码")
    private String yfp_dm;
    @ApiModelProperty("发票号码")
    private String fp_hm;
    @ApiModelProperty("购买方纳税人识别号")
    private String gmf_nsrsbh;
    @ApiModelProperty("发票类型 026-电票 004-专票 007-普票 025-卷票")
    private String itype;
    @ApiModelProperty("校验码")
    private String jym;
    @ApiModelProperty("开票类型 0-蓝字发票 1-红字发票")
    private String kplx;
    @ApiModelProperty("发票清单PDF文件获取key")
    private String pdf_item_key;
    @ApiModelProperty("业务单据号")
    private String order_num;
    @ApiModelProperty("征税方式 0-普通征税 2-差额征税")
    private String zsfs;
    @ApiModelProperty("销售方地址、电话")
    private String xsf_dzdh;
    @ApiModelProperty("税控设备机器编号")
    private String jqbh;
    @ApiModelProperty("合计金额(不含税)")
    private String hjje;
    @ApiModelProperty("购买方地址、电话")
    private String gmf_dzdh;
    @ApiModelProperty("发票请求流水号")
    private String fpqqlsh;
    @ApiModelProperty("收款人")
    private String skr;
    @ApiModelProperty("购买方银行账号")
    private String gmf_yhzh;
    @ApiModelProperty("开票人")
    private String kpr;
    @ApiModelProperty("销售方纳税人识别号")
    private String xsf_nsrsbh;
    @ApiModelProperty("发票密文")
    private String fp_mw;
    @ApiModelProperty("价税合计")
    private String jshj;
    @ApiModelProperty("发票PDF文件获取key")
    private String pdf_key;
    @ApiModelProperty("开票日期(yyyymmddhhmiss)")
    private String kprq;
    @ApiModelProperty("提取码")
    private String ext_code;
    @ApiModelProperty("商品明细")
    private List<InvoiceA0001ResultItemDetail> common_fpkj_xmxx;
}
server/services/src/main/java/com/doumee/dao/dto/invoice/InvoiceA0001ResultItemDetail.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package com.doumee.dao.dto.invoice;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("A0001结果商品明细")
public class InvoiceA0001ResultItemDetail {
    @ApiModelProperty("发票行性质")
    private String fphxz;
    @ApiModelProperty("商品编码")
    private String spbm;
    @ApiModelProperty("自行编码")
    private String zxbm;
    @ApiModelProperty("优惠政策标识")
    private String yhzcbs;
    @ApiModelProperty("零税率标识")
    private String lslbs;
    @ApiModelProperty("增值税特殊管理")
    private String zzstsgl;
    @ApiModelProperty("项目名称")
    private String xmmc;
    @ApiModelProperty("规格型号")
    private String ggxh;
    @ApiModelProperty("计量单位")
    private String dw;
    @ApiModelProperty("项目数量")
    private String xmsl;
    @ApiModelProperty("项目单价(不含税)")
    private String xmdj;
    @ApiModelProperty("项目金额(不含税)")
    private String xmje;
    @ApiModelProperty("税率")
    private String sl;
    @ApiModelProperty("税额")
    private String se;
}
server/services/src/main/java/com/doumee/dao/vo/DataBoardVO.java
@@ -29,4 +29,13 @@
    @ApiModelProperty(value = "行李类型占比")
    private List<LuggageTypeItem> luggageTypeList;
    @ApiModelProperty(value = "门店入驻率(如44.32表示44.32%)")
    private BigDecimal shopSettlementRate;
    @ApiModelProperty(value = "司机通过率(如44.32表示44.32%)")
    private BigDecimal driverPassRate;
    @ApiModelProperty(value = "周期退款单数")
    private Long refundOrderCount;
}
server/services/src/main/java/com/doumee/dao/vo/DriverKpiVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package com.doumee.dao.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
 * å¸æœºæ ¸å¿ƒä¸šç»©æŒ‡æ ‡
 *
 * @Author : Rk
 * @create 2026/05/15
 */
@Data
@ApiModel("司机核心业绩指标")
public class DriverKpiVO implements Serializable {
    @ApiModelProperty("总订单量(异地寄存订单,状态1-7)")
    private Integer totalOrderCount;
    @ApiModelProperty("总完成订单量(状态=7)")
    private Integer finishedOrderCount;
    @ApiModelProperty("总营收金额(分)")
    private Long totalRevenue;
    @ApiModelProperty("司机分成金额(分)")
    private Long driverFeeTotal;
    @ApiModelProperty("退款单数")
    private Integer refundOrderCount;
    @ApiModelProperty("责任扣款总额(分)")
    private Long deductTotal;
}
server/services/src/main/java/com/doumee/dao/vo/DriverOrderTrendVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.doumee.dao.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
 * å¸æœºè¿‘七日订单趋势项
 *
 * @Author : Rk
 * @create 2026/05/15
 */
@Data
@ApiModel("司机近七日订单趋势项")
public class DriverOrderTrendVO implements Serializable {
    @ApiModelProperty("日期(yyyy-MM-dd)")
    private String date;
    @ApiModelProperty("订单数量(状态 3/4/5/7)")
    private Integer orderCount;
}
server/services/src/main/java/com/doumee/dao/vo/DriverRewardHallVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package com.doumee.dao.vo;
import com.doumee.dao.business.model.RewardRecord;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
 * å¸æœºå¥–励大厅
 *
 * @Author : Rk
 * @create 2026/05/15
 */
@Data
@ApiModel("司机奖励大厅")
public class DriverRewardHallVO implements Serializable {
    @ApiModelProperty("已领取奖励金额(分)")
    private Long claimedAmount;
    @ApiModelProperty("待领取奖励金额(分)")
    private Long pendingAmount;
    @ApiModelProperty("注册奖励单数")
    private String registerRewardOrderCount;
    @ApiModelProperty("注册奖励金额(分)")
    private String registerRewardAmount;
    @ApiModelProperty("平台奖励单数")
    private String platformRewardOrderCount;
    @ApiModelProperty("平台奖励金额(分)")
    private String platformRewardAmount;
    @ApiModelProperty("奖励记录列表")
    private List<RewardRecord> records;
}
server/services/src/main/java/com/doumee/dao/vo/DriverTopVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.doumee.dao.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
@ApiModel("司机业绩Top10项")
public class DriverTopVO implements Serializable {
    @ApiModelProperty(value = "司机ID")
    private Integer driverId;
    @ApiModelProperty(value = "司机姓名")
    private String driverName;
    @ApiModelProperty(value = "总完成订单量")
    private Long completedCount;
    @ApiModelProperty(value = "总营收金额(元)")
    private BigDecimal totalRevenue;
    @ApiModelProperty(value = "司机分成总额(元)")
    private BigDecimal driverFeeTotal;
    @ApiModelProperty(value = "退款单数")
    private Long refundCount;
    @ApiModelProperty(value = "责任扣款总额(元)")
    private BigDecimal penaltyAmount;
}
server/services/src/main/java/com/doumee/dao/vo/FinanceOverviewVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package com.doumee.dao.vo;
import com.doumee.core.annotation.excel.ExcelColumn;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
@ApiModel("平台财务总览项")
public class FinanceOverviewVO implements Serializable {
    @ExcelColumn(name = "年月", width = 14, index = 1)
    @ApiModelProperty(value = "年月(如2026-01)")
    private String date;
    @ExcelColumn(name = "寄存订单营收(元)", width = 18, index = 2)
    @ApiModelProperty(value = "寄存订单营收(元)")
    private BigDecimal storageRevenue;
    @ExcelColumn(name = "寄送订单营收(元)", width = 18, index = 3)
    @ApiModelProperty(value = "寄送订单营收(元)")
    private BigDecimal deliveryRevenue;
    @ExcelColumn(name = "平台总营收(元)", width = 18, index = 4)
    @ApiModelProperty(value = "平台总营收(元)")
    private BigDecimal totalRevenue;
    @ExcelColumn(name = "门店分成总额(元)", width = 18, index = 5)
    @ApiModelProperty(value = "门店分成总额(元)")
    private BigDecimal shopFeeTotal;
    @ExcelColumn(name = "司机分成总额(元)", width = 18, index = 6)
    @ApiModelProperty(value = "司机分成总额(元)")
    private BigDecimal driverFeeTotal;
    @ExcelColumn(name = "退款总金额(元)", width = 18, index = 7)
    @ApiModelProperty(value = "退款总金额(元)")
    private BigDecimal refundAmount;
    @ExcelColumn(name = "逾期费用(元)", width = 18, index = 8)
    @ApiModelProperty(value = "逾期费用(元)")
    private BigDecimal overdueAmount;
    @ExcelColumn(name = "平台净营收(元)", width = 18, index = 9)
    @ApiModelProperty(value = "平台净营收(元)")
    private BigDecimal netRevenue;
}
server/services/src/main/java/com/doumee/dao/vo/InvoiceRecordSummaryVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.doumee.dao.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
 * å‘票统计汇总
 *
 * @author rk
 * @date 2026/05/18
 */
@Data
@ApiModel("发票统计汇总")
public class InvoiceRecordSummaryVO implements Serializable {
    @ApiModelProperty("开票总额(分)= ç¬¦åˆæ¡ä»¶çš„æ‰€æœ‰è®°å½• sum(invoiceAmount)")
    private Long totalAmount;
    @ApiModelProperty("已开票总额(分)= status=1 çš„ sum(invoiceAmount)")
    private Long issuedAmount;
}
server/services/src/main/java/com/doumee/dao/vo/LocationTagShopCountVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,27 @@
package com.doumee.dao.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
 * ä½ç½®æ ‡ç­¾é—¨åº—数量统计
 *
 * @Author : Rk
 * @create 2026/05/15
 */
@Data
@ApiModel("位置标签门店数量统计")
public class LocationTagShopCountVO implements Serializable {
    @ApiModelProperty("标签主键")
    private Integer tagId;
    @ApiModelProperty("标签名称")
    private String tagName;
    @ApiModelProperty("门店数量")
    private Integer shopCount;
}
server/services/src/main/java/com/doumee/dao/vo/LuggageTypeItem.java
@@ -1,5 +1,6 @@
package com.doumee.dao.vo;
import com.doumee.core.annotation.excel.ExcelColumn;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -10,12 +11,15 @@
@ApiModel("行李类型占比项")
public class LuggageTypeItem implements Serializable {
    @ExcelColumn(name = "类型名称", width = 20, index = 1)
    @ApiModelProperty(value = "类型名称")
    private String luggageName;
    @ExcelColumn(name = "订单数", width = 12, index = 2)
    @ApiModelProperty(value = "订单数")
    private Long orderCount;
    @ExcelColumn(name = "行李数", width = 12, index = 3)
    @ApiModelProperty(value = "行李数")
    private Long luggageCount;
}
server/services/src/main/java/com/doumee/dao/vo/MemberListVO.java
@@ -41,4 +41,8 @@
    @ApiModelProperty(value = "状态 0=正常;1=停用;2=已注销")
    private Integer status;
    @ExcelColumn(name = "邮箱", index = 6, width = 10)
    @ApiModelProperty(value = "邮箱")
    private String email;
}
server/services/src/main/java/com/doumee/dao/vo/MyOrderVO.java
@@ -61,6 +61,9 @@
    @ApiModelProperty(value = "存件门店联系电话")
    private String depositShopPhone;
    @ApiModelProperty(value = "存件门店地址")
    private String depositShopAddress;
    // ---- å–件信息 ----
    @ApiModelProperty(value = "取件门店主键(有取件门店时返回)")
@@ -139,4 +142,10 @@
    @ApiModelProperty(value = "下单照片")
    private List<String> orderImages;
    @ApiModelProperty(value = "可开票金额(分)= æ”¯ä»˜é‡‘额 - é€€æ¬¾é‡‘额")
    private Long invoiceAmount;
    @ApiModelProperty(value = "优惠券抵扣金额(分)")
    private Long deductionAmount;
}
server/services/src/main/java/com/doumee/dao/vo/OrderDetailVO.java
@@ -79,4 +79,7 @@
    @ApiModelProperty(value = "序号")
    private String sortnum;
    @ApiModelProperty(value = "优惠券抵扣金额(分)")
    private Long deductionAmount;
}
server/services/src/main/java/com/doumee/dao/vo/OrderSummaryVO.java
@@ -25,4 +25,7 @@
    @ApiModelProperty(value = "司机已结算费用(分)")
    private Long settledDriverFeeSum;
    @ApiModelProperty(value = "实付金额合计(分)= æ”¯ä»˜é‡‘额 - é€€æ¬¾é‡‘额 + é€¾æœŸè´¹ç”¨")
    private Long actualPayAmountSum;
}
server/services/src/main/java/com/doumee/dao/vo/OrdersExportVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package com.doumee.dao.vo;
import com.doumee.core.annotation.excel.ExcelColumn;
import lombok.Data;
import java.util.Date;
@Data
public class OrdersExportVO {
    @ExcelColumn(name = "订单编号", index = 0)
    private String code;
    @ExcelColumn(name = "物品信息", index = 1)
    private String goodsInfo;
    @ExcelColumn(name = "类型", index = 2)
    private String typeName;
    @ExcelColumn(name = "订单级别", index = 3)
    private String orderLevel;
    @ExcelColumn(name = "物品保费(元)", index = 4)
    private String declaredFee;
    @ExcelColumn(name = "基础服务费(元)", index = 5)
    private String basicAmount;
    @ExcelColumn(name = "订单总价(元)", index = 6)
    private String totalAmount;
    @ExcelColumn(name = "实付现金(元)", index = 7)
    private String payAmount;
    @ExcelColumn(name = "加急费(元)", index = 8)
    private String urgentAmount;
    @ExcelColumn(name = "退款金额(元)", index = 9)
    private String refundAmount;
    @ExcelColumn(name = "超时金额(元)", index = 10)
    private String overdueAmount;
    @ExcelColumn(name = "异常金额(元)", index = 11)
    private String exceptionAmount;
    @ExcelColumn(name = "优惠券折扣(元)", index = 12)
    private String deductionAmount;
    @ExcelColumn(name = "订单状态", index = 13)
    private String statusDesc;
    @ExcelColumn(name = "结算状态", index = 14)
    private String settlementDesc;
    @ExcelColumn(name = "支付时间", index = 15, dateFormat = "yyyy-MM-dd HH:mm:ss", width = 16)
    private Date payTime;
    @ExcelColumn(name = "创建时间", index = 16, dateFormat = "yyyy-MM-dd HH:mm:ss", width = 16)
    private Date createTime;
}
server/services/src/main/java/com/doumee/dao/vo/PriceCalculateVO.java
@@ -1,5 +1,6 @@
package com.doumee.dao.vo;
import com.doumee.dao.business.model.MemberCoupon;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -43,4 +44,13 @@
    @ApiModelProperty("天数")
    private Integer days;
    @ApiModelProperty("优惠金额(分)")
    private Long deductionAmount;
    @ApiModelProperty("选中的优惠券信息")
    private MemberCoupon selectedCoupon;
    @ApiModelProperty("可用优惠券列表(金额倒序、到期时间顺序)")
    private List<MemberCoupon> availableCoupons;
}
server/services/src/main/java/com/doumee/dao/vo/ShopDetailVO.java
@@ -190,4 +190,31 @@
    @ApiModelProperty(value = "关联正式版本门店主键")
    private Integer relationShopId;
    @ApiModelProperty(value = "位置标签主键(逗号分隔)")
    private String locationTagIds;
    @ApiModelProperty(value = "位置标签名称(逗号分隔)")
    private String locationTagNames;
    @ApiModelProperty(value = "门店头像")
    private String coverImg;
    @ApiModelProperty(value = "门店头像(全路径)")
    private String coverImgUrl;
    @ApiModelProperty(value = "门店介绍")
    private String content;
    @ApiModelProperty(value = "寄存类型")
    private String depositTypes;
    @ApiModelProperty(value = "收费标准")
    private String feeStandard;
    @ApiModelProperty(value = "门店营业时间")
    private String shopHours;
    @ApiModelProperty(value = "门店营业类型:0=非全天;1=全天")
    private Integer businessType;
}
server/services/src/main/java/com/doumee/dao/vo/ShopKpiVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package com.doumee.dao.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
 * é—¨åº—核心业绩指标
 *
 * @Author : Rk
 * @create 2026/05/15
 */
@Data
@ApiModel("门店核心业绩指标")
public class ShopKpiVO implements Serializable {
    @ApiModelProperty("寄存订单量(就地寄存,状态1-7)")
    private Integer localOrderCount;
    @ApiModelProperty("寄送订单量(异地寄存,状态1-7)")
    private Integer remoteOrderCount;
    @ApiModelProperty("总订单量")
    private Integer totalOrderCount;
    @ApiModelProperty("总完成订单量(状态=7)")
    private Integer finishedOrderCount;
    @ApiModelProperty("总营收金额(分)")
    private Long totalRevenue;
    @ApiModelProperty("门店分成金额(分)")
    private Long shopFeeTotal;
    @ApiModelProperty("退款单数")
    private Integer refundOrderCount;
    @ApiModelProperty("责任扣款总额(分)")
    private Long deductTotal;
}
server/services/src/main/java/com/doumee/dao/vo/ShopNearbyVO.java
@@ -42,4 +42,8 @@
    @ApiModelProperty(value = "门店经度")
    private Double longitude;
}
    @ApiModelProperty(value = "位置标签名称(逗号分隔)")
    private String locationTagNames;
}
server/services/src/main/java/com/doumee/dao/vo/ShopTopVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.doumee.dao.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
@ApiModel("门店业绩Top10项")
public class ShopTopVO implements Serializable {
    @ApiModelProperty(value = "门店ID")
    private Integer shopId;
    @ApiModelProperty(value = "门店名称")
    private String shopName;
    @ApiModelProperty(value = "总完成订单量")
    private Long completedCount;
    @ApiModelProperty(value = "总营收金额(元)")
    private BigDecimal totalRevenue;
    @ApiModelProperty(value = "门店分成总额(元)")
    private BigDecimal shopFeeTotal;
    @ApiModelProperty(value = "退款单数")
    private Long refundCount;
    @ApiModelProperty(value = "责任扣款总额(元)")
    private BigDecimal penaltyAmount;
}
server/services/src/main/java/com/doumee/dao/vo/ShopWebDetailVO.java
@@ -48,4 +48,7 @@
    @ApiModelProperty(value = "门店联系电话")
    private String linkPhone;
    @ApiModelProperty(value = "位置标签名称(逗号分隔)")
    private String locationTagNames;
}
server/services/src/main/java/com/doumee/dao/vo/UserCenterVO.java
@@ -51,6 +51,9 @@
    @ApiModelProperty(value = "我当前绑定的门店")
    private String bindShopId;
    @ApiModelProperty(value = "邮箱")
    private String email;
}
server/services/src/main/java/com/doumee/service/business/DataBoardService.java
@@ -1,6 +1,8 @@
package com.doumee.service.business;
import com.doumee.dao.dto.DataBoardQueryDTO;
import com.doumee.dao.dto.FinanceQueryDTO;
import com.doumee.dao.dto.TrendQueryDTO;
import com.doumee.dao.vo.*;
import java.util.List;
@@ -9,11 +11,24 @@
    DataBoardVO overview(DataBoardQueryDTO query);
    List<MemberTrendVO> memberTrend();
    List<MemberTrendVO> memberTrend(TrendQueryDTO query);
    List<OrderTrendVO> orderTrend();
    List<OrderTrendVO> orderTrend(TrendQueryDTO query);
    List<RevenueTrendVO> revenueTrend();
    List<RevenueTrendVO> revenueTrend(TrendQueryDTO query);
    ShopPerformanceVO shopPerformance(DataBoardQueryDTO query);
    List<LuggageTypeItem> luggageTypeList(DataBoardQueryDTO query);
    List<FinanceOverviewVO> financeOverview(FinanceQueryDTO query);
    List<ShopTopVO> shopTop(TrendQueryDTO query);
    List<DriverTopVO> driverTop(TrendQueryDTO query);
    /**
     * æ ¹æ®è®¢å•ID列表构建行李类型分布
     */
    List<LuggageTypeItem> buildLuggageTypeList(List<Integer> orderIds);
}
server/services/src/main/java/com/doumee/service/business/InvoiceRecordService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
package com.doumee.service.business;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.InvoiceRecord;
import com.doumee.dao.dto.ApplyInvoiceDTO;
import com.doumee.dao.vo.InvoiceRecordSummaryVO;
import java.util.List;
/**
 * å‘票申请记录Service定义
 *
 * @author rk
 * @date 2026/05/18
 */
public interface InvoiceRecordService {
    Integer create(InvoiceRecord invoiceRecord);
    void deleteById(Integer id);
    void delete(InvoiceRecord invoiceRecord);
    void deleteByIdInBatch(List<Integer> ids);
    void updateById(InvoiceRecord invoiceRecord);
    void updateByIdInBatch(List<InvoiceRecord> invoiceRecords);
    InvoiceRecord findById(Integer id);
    InvoiceRecord findOne(InvoiceRecord invoiceRecord);
    List<InvoiceRecord> findList(InvoiceRecord invoiceRecord);
    PageData<InvoiceRecord> findPage(PageWrap<InvoiceRecord> pageWrap);
    long count(InvoiceRecord invoiceRecord);
    /**
     * ä¼šå‘˜ç”³è¯·å¼€ç¥¨
     *
     * @param dto      å¼€ç¥¨ç”³è¯·è¯·æ±‚
     * @param memberId ä¼šå‘˜ä¸»é”®
     */
    void applyInvoice(ApplyInvoiceDTO dto, Integer memberId);
    /**
     * ä¼šå‘˜å¼€ç¥¨è®°å½•分页(按创建时间倒序)
     */
    PageData<InvoiceRecord> findMemberInvoicePage(PageWrap<InvoiceRecord> pageWrap, Integer memberId);
    /**
     * å‘送发票邮件
     */
    void sendInvoiceEmail(Integer memberId, Integer invoiceRecordId, String email);
    /**
     * å‘票统计汇总(与 findPage ç›¸åŒç­›é€‰æ¡ä»¶ï¼‰
     */
    InvoiceRecordSummaryVO findPageSummary(PageWrap<InvoiceRecord> pageWrap);
}
server/services/src/main/java/com/doumee/service/business/MemberCouponService.java
@@ -23,4 +23,8 @@
    PageData<MemberCoupon> findPage(PageWrap<MemberCoupon> pageWrap);
    long count(MemberCoupon memberCoupon);
    PageData<MemberCoupon> findMemberPage(Integer memberId, Integer status, PageWrap<MemberCoupon> pageWrap);
    void claimCoupon(Integer memberId, Integer couponId);
}
server/services/src/main/java/com/doumee/service/business/OrdersService.java
@@ -3,9 +3,12 @@
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.Orders;
import com.doumee.dao.business.model.OrdersRefund;
import com.doumee.dao.dto.CalculateLocalPriceDTO;
import com.doumee.dao.dto.CalculateRemotePriceDTO;
import com.doumee.dao.dto.CreateOrderDTO;
import com.doumee.dao.dto.HandleOrderExceptionDTO;
import com.doumee.dao.dto.ManualRefundDTO;
import com.doumee.dao.dto.DispatchDTO;
import com.doumee.dao.dto.CommentOrderDTO;
import com.doumee.dao.dto.MyOrderDTO;
@@ -359,6 +362,16 @@
    void settleOrders();
    /**
     * æ‰‹åŠ¨é€€æ¬¾
     */
    void manualRefund(ManualRefundDTO dto, Integer userId);
    /**
     * å¤„理手动退款的扣款逻辑(退款回调成功时调用)
     */
    void processManualRefundCallback(OrdersRefund refundRecord);
    /**
     * è®¢å•评价
     * status=7且commentStatus=0时可评价,按对象(存件门店/取件门店/司机)分别记录评分
     *
@@ -438,6 +451,11 @@
    int notifyArrivalPickUp();
    /**
     * è®¢å•异常处理:异地无取件门店已送达订单,创建就地存取新订单
     */
    void handleOrderException(HandleOrderExceptionDTO dto);
    /**
     * æ ¡éªŒæ“ä½œåŠå¾„
     * @param orderId     è®¢å•主键
     * @param userId      æ“ä½œç”¨æˆ·ä¸»é”®
server/services/src/main/java/com/doumee/service/business/RevenueService.java
@@ -4,6 +4,9 @@
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.Revenue;
import com.doumee.dao.dto.RevenueQueryDTO;
import com.doumee.dao.vo.DriverKpiVO;
import com.doumee.dao.vo.DriverOrderTrendVO;
import com.doumee.dao.vo.DriverRewardHallVO;
import com.doumee.dao.vo.RevenueStatisticsVO;
import com.doumee.dao.vo.RevenueSummaryVO;
@@ -151,4 +154,37 @@
     */
    RevenueSummaryVO getShopRevenueSummary(RevenueQueryDTO queryDTO, Integer shopId);
    /**
     * å¸æœºæ ¸å¿ƒä¸šç»©æŒ‡æ ‡
     *
     * @param driverId å¸æœºä¸»é”®
     * @param query    æ—¥æœŸèŒƒå›´
     * @return KPI
     */
    DriverKpiVO getDriverKpi(Integer driverId, com.doumee.dao.dto.ShopRevenueQueryDTO query);
    /**
     * å¸æœºè¿‘七日订单趋势
     *
     * @param driverId å¸æœºä¸»é”®
     * @return 7天趋势数据
     */
    List<DriverOrderTrendVO> getDriverOrderTrend(Integer driverId);
    /**
     * å¸æœºå¥–励大厅
     *
     * @param driverId å¸æœºä¸»é”®
     * @return å¥–励大厅数据
     */
    DriverRewardHallVO getDriverRewardHall(Integer driverId);
    /**
     * é¢†å–奖励金
     *
     * @param driverId       å¸æœºä¸»é”®
     * @param rewardRecordId å¥–励记录ID
     */
    void claimReward(Integer driverId, Integer rewardRecordId);
}
server/services/src/main/java/com/doumee/service/business/ShopInfoService.java
@@ -5,6 +5,9 @@
import com.doumee.dao.business.model.ShopInfo;
import com.doumee.dao.business.model.Member;
import com.doumee.dao.dto.*;
import com.doumee.dao.vo.LocationTagShopCountVO;
import com.doumee.dao.vo.LuggageTypeItem;
import com.doumee.dao.vo.ShopKpiVO;
import com.doumee.dao.vo.ShopLoginVO;
import com.doumee.dao.vo.ShopCenterVO;
import com.doumee.dao.vo.ShopDetailVO;
@@ -158,6 +161,13 @@
    PageData<ShopNearbyVO> findNearbyShops(PageWrap<ShopNearbyDTO> pageWrap);
    /**
     * æŒ‰ä½ç½®æ ‡ç­¾ç»Ÿè®¡é—¨åº—数量
     * @param cityId åŸŽå¸‚主键(可选)
     * @return æ ‡ç­¾é—¨åº—数量列表
     */
    List<LocationTagShopCountVO> countShopsByLocationTag(Integer cityId);
    /**
     * æŸ¥è¯¢é—¨åº—详情(小程序端,含照片集合和可选距离)
     * @param dto æŸ¥è¯¢è¯·æ±‚
     * @return é—¨åº—详情
@@ -210,6 +220,16 @@
    ShopSalesStatsVO getShopSalesStats(Integer shopId, Integer period);
    /**
     * é—¨åº—行李类型分布
     */
    List<LuggageTypeItem> shopLuggageTypeList(Integer shopId, ShopRevenueQueryDTO query);
    /**
     * é—¨åº—核心业绩指标
     */
    ShopKpiVO getShopKpi(Integer shopId, ShopRevenueQueryDTO query);
    /**
     * é—¨åº—修改密码
     *
     * @param shopId      é—¨åº—主键
server/services/src/main/java/com/doumee/service/business/impl/CategoryServiceImpl.java
@@ -61,7 +61,7 @@
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        // type=2(寄存说明)detail不必填,其他类型必填
        if (!Constants.equalsInteger(category.getType(), Constants.TWO) && StringUtils.isBlank(category.getDetail())) {
        if (!Constants.equalsInteger(category.getType(), Constants.TWO)&& !Constants.equalsInteger(category.getType(), Constants.FIVE) && StringUtils.isBlank(category.getDetail())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        validateByType(category);
@@ -107,7 +107,7 @@
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        // type=2(寄存说明)detail不必填,其他类型必填
        if (!Constants.equalsInteger(category.getType(), Constants.TWO) && StringUtils.isBlank(category.getDetail())) {
        if (!Constants.equalsInteger(category.getType(), Constants.TWO) && !Constants.equalsInteger(category.getType(), Constants.FIVE) && StringUtils.isBlank(category.getDetail())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
        validateByType(category);
server/services/src/main/java/com/doumee/service/business/impl/DataBoardServiceImpl.java
@@ -6,6 +6,8 @@
import com.doumee.dao.business.*;
import com.doumee.dao.business.model.*;
import com.doumee.dao.dto.DataBoardQueryDTO;
import com.doumee.dao.dto.FinanceQueryDTO;
import com.doumee.dao.dto.TrendQueryDTO;
import com.doumee.dao.vo.*;
import com.doumee.service.business.DataBoardService;
import lombok.extern.slf4j.Slf4j;
@@ -37,14 +39,17 @@
    private OtherOrdersMapper otherOrdersMapper;
    @Autowired
    private OrdersRefundMapper ordersRefundMapper;
    @Autowired
    private RevenueMapper revenueMapper;
    @Override
    public DataBoardVO overview(DataBoardQueryDTO query) {
        query.resolveDateRange();
        DataBoardVO vo = new DataBoardVO();
        // ä¼šå‘˜æ€»æ•°ï¼ˆä¸å—æ—¶é—´/门店影响)
        // ä¼šå‘˜æ€»æ•°ï¼ˆä¸å—æ—¶é—´/门店影响,userType=0 ä¼šå‘˜èº«ä»½ï¼‰
        vo.setMemberCount(memberMapper.selectCount(new QueryWrapper<Member>().lambda()
                .eq(Member::getDeleted, Constants.ZERO)
                .eq(Member::getStatus, Constants.ZERO)));
                .eq(Member::getUserType, Constants.ZERO)));
        // é—¨åº—总数(auditStatus=3 æ­£å¼ç‰ˆæœ¬ï¼‰
        vo.setShopCount(shopInfoMapper.selectCount(new QueryWrapper<ShopInfo>().lambda()
                .eq(ShopInfo::getAuditStatus, Constants.THREE)
@@ -89,40 +94,63 @@
        // è¡ŒæŽç±»åž‹å æ¯”
        List<Integer> orderIds = orders.stream().map(Orders::getId).collect(Collectors.toList());
        List<LuggageTypeItem> luggageTypeList = new ArrayList<>();
        if (!orderIds.isEmpty()) {
            List<OrdersDetail> details = ordersDetailMapper.selectList(new QueryWrapper<OrdersDetail>().lambda()
                    .in(OrdersDetail::getOrderId, orderIds)
                    .eq(OrdersDetail::getDeleted, Constants.ZERO));
            Map<String, List<OrdersDetail>> grouped = details.stream()
                    .filter(d -> StringUtils.isNotBlank(d.getLuggageName()))
                    .collect(Collectors.groupingBy(OrdersDetail::getLuggageName));
            for (Map.Entry<String, List<OrdersDetail>> entry : grouped.entrySet()) {
                LuggageTypeItem item = new LuggageTypeItem();
                item.setLuggageName(entry.getKey());
                item.setOrderCount((long) entry.getValue().stream().map(OrdersDetail::getOrderId).distinct().count());
                item.setLuggageCount((long) entry.getValue().stream()
                        .mapToInt(d -> d.getNum() != null ? d.getNum() : 0).sum());
                luggageTypeList.add(item);
            }
        vo.setLuggageTypeList(buildLuggageTypeList(orderIds));
        // é—¨åº—入驻率:auditStatus=3 çš„门店数 / å…¨éƒ¨é—¨åº—æ•°
        long totalShopCount = shopInfoMapper.selectCount(new QueryWrapper<ShopInfo>().lambda()
                .eq(ShopInfo::getVersionType, Constants.ZERO)
                .eq(ShopInfo::getDeleted, Constants.ZERO));
        if (totalShopCount > 0) {
            vo.setShopSettlementRate(BigDecimal.valueOf(vo.getShopCount())
                    .multiply(BigDecimal.valueOf(100))
                    .divide(BigDecimal.valueOf(totalShopCount), 2, RoundingMode.HALF_UP));
        } else {
            vo.setShopSettlementRate(BigDecimal.ZERO);
        }
        vo.setLuggageTypeList(luggageTypeList);
        // å¸æœºé€šè¿‡çŽ‡ï¼šauditStatus=3 çš„司机数 / å…¨éƒ¨å¸æœºæ•°
        long totalDriverCount = driverInfoMapper.selectCount(new QueryWrapper<DriverInfo>().lambda()
                .eq(DriverInfo::getVersionType, Constants.ZERO)
                .eq(DriverInfo::getDeleted, Constants.ZERO));
        if (totalDriverCount > 0) {
            vo.setDriverPassRate(BigDecimal.valueOf(vo.getDriverCount())
                    .multiply(BigDecimal.valueOf(100))
                    .divide(BigDecimal.valueOf(totalDriverCount), 2, RoundingMode.HALF_UP));
        } else {
            vo.setDriverPassRate(BigDecimal.ZERO);
        }
        // å‘¨æœŸé€€æ¬¾å•æ•°
        if (!orderIds.isEmpty()) {
            QueryWrapper<OrdersRefund> refundQw = new QueryWrapper<>();
            refundQw.lambda()
                    .in(OrdersRefund::getOrderId, orderIds)
                    .eq(OrdersRefund::getStatus, Constants.ONE)
                    .eq(OrdersRefund::getDeleted, Constants.ZERO);
            List<OrdersRefund> refundRecords = ordersRefundMapper.selectList(refundQw);
            vo.setRefundOrderCount(refundRecords.stream()
                    .map(OrdersRefund::getOrderId).distinct().count());
        } else {
            vo.setRefundOrderCount(0L);
        }
        return vo;
    }
    @Override
    public List<MemberTrendVO> memberTrend() {
        Date startDate = get30DaysAgoStart();
    public List<MemberTrendVO> memberTrend(TrendQueryDTO query) {
        TrendDateRange range = parseTrendDateRange(query);
        List<Member> members = memberMapper.selectList(new QueryWrapper<Member>().lambda()
                .eq(Member::getDeleted, Constants.ZERO)
                .eq(Member::getStatus, Constants.ZERO)
                .ge(Member::getCreateTime, startDate));
                .eq(Member::getUserType, Constants.ZERO)
                .ge(Member::getCreateTime, range.startDate)
                .le(Member::getCreateTime, range.endDate));
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        SimpleDateFormat sdf = new SimpleDateFormat(range.pattern);
        Map<String, Long> map = members.stream()
                .collect(Collectors.groupingBy(m -> sdf.format(m.getCreateTime()), Collectors.counting()));
        return buildDailyList(startDate, (date) -> {
        return buildTrendList(range, (date) -> {
            MemberTrendVO vo = new MemberTrendVO();
            vo.setDate(date);
            vo.setCount(map.getOrDefault(date, 0L));
@@ -131,24 +159,17 @@
    }
    @Override
    public List<OrderTrendVO> orderTrend() {
        Date startDate = get30DaysAgoStart();
    public List<OrderTrendVO> orderTrend(TrendQueryDTO query) {
        TrendDateRange range = parseTrendDateRange(query);
        List<Orders> orders = ordersMapper.selectList(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .in(Orders::getStatus,
                                Constants.OrderStatus.waitDeposit.getKey(),
                                Constants.OrderStatus.deposited.getKey(),
                                Constants.OrderStatus.accepted.getKey(),
                                Constants.OrderStatus.delivering.getKey(),
                                Constants.OrderStatus.arrived.getKey(),
                                Constants.OrderStatus.overdue.getKey())
                .ge(Orders::getCreateTime, startDate));
                .ge(Orders::getCreateTime, range.startDate)
                .le(Orders::getCreateTime, range.endDate));
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        SimpleDateFormat sdf = new SimpleDateFormat(range.pattern);
        Map<String, List<Orders>> grouped = orders.stream()
                .collect(Collectors.groupingBy(o -> sdf.format(o.getCreateTime())));
        return buildDailyList(startDate, (date) -> {
        return buildTrendList(range, (date) -> {
            List<Orders> dayOrders = grouped.getOrDefault(date, Collections.emptyList());
            OrderTrendVO vo = new OrderTrendVO();
            vo.setDate(date);
@@ -159,35 +180,45 @@
    }
    @Override
    public List<RevenueTrendVO> revenueTrend() {
        Date startDate = get30DaysAgoStart();
    public List<RevenueTrendVO> revenueTrend(TrendQueryDTO query) {
        TrendDateRange range = parseTrendDateRange(query);
        SimpleDateFormat sdf = new SimpleDateFormat(range.pattern);
        List<Orders> orders = ordersMapper.selectList(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .in(Orders::getStatus,
                                Constants.OrderStatus.waitDeposit.getKey(),
                                Constants.OrderStatus.deposited.getKey(),
                                Constants.OrderStatus.accepted.getKey(),
                                Constants.OrderStatus.delivering.getKey(),
                                Constants.OrderStatus.arrived.getKey(),
                                Constants.OrderStatus.overdue.getKey())
                .ge(Orders::getCreateTime, startDate));
                        Constants.OrderStatus.waitDeposit.getKey(),
                        Constants.OrderStatus.deposited.getKey(),
                        Constants.OrderStatus.accepted.getKey(),
                        Constants.OrderStatus.delivering.getKey(),
                        Constants.OrderStatus.arrived.getKey(),
                        Constants.OrderStatus.overdue.getKey(),
                        Constants.OrderStatus.finished.getKey())
                .ge(Orders::getCreateTime, range.startDate)
                .le(Orders::getCreateTime, range.endDate));
        List<OtherOrders> otherOrders = otherOrdersMapper.selectList(new QueryWrapper<OtherOrders>().lambda()
                .eq(OtherOrders::getType, 2)
                .eq(OtherOrders::getPayStatus, Constants.ONE)
                .eq(OtherOrders::getDeleted, Constants.ZERO)
                .ge(OtherOrders::getPayTime, startDate));
        // æ ¹æ®ç­›é€‰å‡ºçš„订单主键查询已支付的逾期费用
        List<Integer> orderIds = orders.stream().map(Orders::getId).collect(Collectors.toList());
        Map<Integer, Long> overduePaidMap = new HashMap<>();
        if (!orderIds.isEmpty()) {
            List<OtherOrders> overdueOrders = otherOrdersMapper.selectList(new QueryWrapper<OtherOrders>().lambda()
                    .eq(OtherOrders::getType, 2)
                    .eq(OtherOrders::getPayStatus, Constants.ONE)
                    .eq(OtherOrders::getDeleted, Constants.ZERO)
                    .in(OtherOrders::getOrderId, orderIds));
            for (OtherOrders oo : overdueOrders) {
                long amt = oo.getPayAccount() != null ? oo.getPayAccount() : 0L;
                overduePaidMap.merge(oo.getOrderId(), amt, Long::sum);
            }
        }
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Map<String, Long> localOrderRevenue = new HashMap<>();
        Map<String, Long> remoteOrderRevenue = new HashMap<>();
        for (Orders o : orders) {
            String date = sdf.format(o.getCreateTime());
            long pay = o.getPayAmount() != null ? o.getPayAmount() : 0L;
            long refund = o.getRefundAmount() != null ? o.getRefundAmount() : 0L;
            long overdue = (o.getOverdueStatus() != null && o.getOverdueStatus() == 2 && o.getOverdueAmount() != null)
                    ? o.getOverdueAmount() : 0L;
            long overdue = overduePaidMap.getOrDefault(o.getId(), 0L);
            long rev = pay - refund + overdue;
            if (Constants.equalsInteger(o.getType(), Constants.ZERO)) {
                localOrderRevenue.merge(date, rev, Long::sum);
@@ -196,17 +227,10 @@
            }
        }
        Map<String, Long> otherRevenueMap = new HashMap<>();
        for (OtherOrders oo : otherOrders) {
            String date = sdf.format(oo.getPayTime());
            long amt = oo.getPayAccount() != null ? oo.getPayAccount() : 0L;
            otherRevenueMap.merge(date, amt, Long::sum);
        }
        return buildDailyList(startDate, (date) -> {
        return buildTrendList(range, (date) -> {
            RevenueTrendVO vo = new RevenueTrendVO();
            vo.setDate(date);
            long local = localOrderRevenue.getOrDefault(date, 0L) + otherRevenueMap.getOrDefault(date, 0L);
            long local = localOrderRevenue.getOrDefault(date, 0L);
            long remote = remoteOrderRevenue.getOrDefault(date, 0L);
            vo.setLocalRevenue(BigDecimal.valueOf(local).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP));
            vo.setRemoteRevenue(BigDecimal.valueOf(remote).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP));
@@ -216,6 +240,7 @@
    @Override
    public ShopPerformanceVO shopPerformance(DataBoardQueryDTO query) {
        query.resolveDateRange();
        ShopPerformanceVO vo = new ShopPerformanceVO();
        // 1. æ€»è®¢å•æ•° + è¥æ”¶ï¼ˆstatus 1-6)
@@ -282,6 +307,295 @@
        return vo;
    }
    @Override
    public List<LuggageTypeItem> luggageTypeList(DataBoardQueryDTO query) {
        query.resolveDateRange();
        List<Orders> orders = ordersMapper.selectList(buildOrderQueryWrapper(query));
        List<Integer> orderIds = orders.stream().map(Orders::getId).collect(Collectors.toList());
        return buildLuggageTypeList(orderIds);
    }
    @Override
    public List<LuggageTypeItem> buildLuggageTypeList(List<Integer> orderIds) {
        List<LuggageTypeItem> luggageTypeList = new ArrayList<>();
        if (orderIds != null && !orderIds.isEmpty()) {
            List<OrdersDetail> details = ordersDetailMapper.selectList(new QueryWrapper<OrdersDetail>().lambda()
                    .in(OrdersDetail::getOrderId, orderIds)
                    .eq(OrdersDetail::getDeleted, Constants.ZERO));
            Map<String, List<OrdersDetail>> grouped = details.stream()
                    .filter(d -> StringUtils.isNotBlank(d.getLuggageName()))
                    .collect(Collectors.groupingBy(OrdersDetail::getLuggageName));
            for (Map.Entry<String, List<OrdersDetail>> entry : grouped.entrySet()) {
                LuggageTypeItem item = new LuggageTypeItem();
                item.setLuggageName(entry.getKey());
                item.setOrderCount((long) entry.getValue().stream().map(OrdersDetail::getOrderId).distinct().count());
                item.setLuggageCount((long) entry.getValue().stream()
                        .mapToInt(d -> d.getNum() != null ? d.getNum() : 0).sum());
                luggageTypeList.add(item);
            }
        }
        return luggageTypeList;
    }
    @Override
    public List<FinanceOverviewVO> financeOverview(FinanceQueryDTO query) {
        // è¡¥é½æ—¥æœŸï¼šstartDate å–月初,endDate å–月末
        Calendar startCal = Calendar.getInstance();
        startCal.setTime(query.getStartDate());
        startCal.set(Calendar.DAY_OF_MONTH, 1);
        startCal.set(Calendar.HOUR_OF_DAY, 0);
        startCal.set(Calendar.MINUTE, 0);
        startCal.set(Calendar.SECOND, 0);
        startCal.set(Calendar.MILLISECOND, 0);
        query.setStartDate(startCal.getTime());
        Calendar endCal = Calendar.getInstance();
        endCal.setTime(query.getEndDate());
        endCal.set(Calendar.DAY_OF_MONTH, endCal.getActualMaximum(Calendar.DAY_OF_MONTH));
        endCal.set(Calendar.HOUR_OF_DAY, 23);
        endCal.set(Calendar.MINUTE, 59);
        endCal.set(Calendar.SECOND, 59);
        endCal.set(Calendar.MILLISECOND, 999);
        query.setEndDate(endCal.getTime());
        List<Orders> orders = ordersMapper.selectList(buildFinanceOrderQueryWrapper(query));
        // æ ¹æ®ç­›é€‰å‡ºçš„订单主键查询已支付的逾期费用
        List<Integer> orderIds = orders.stream().map(Orders::getId).collect(Collectors.toList());
        Map<Integer, Long> overduePaidMap = new HashMap<>();
        if (!orderIds.isEmpty()) {
            List<OtherOrders> overdueOrders = otherOrdersMapper.selectList(new QueryWrapper<OtherOrders>().lambda()
                    .eq(OtherOrders::getType, 2)
                    .eq(OtherOrders::getPayStatus, Constants.ONE)
                    .eq(OtherOrders::getDeleted, Constants.ZERO)
                    .in(OtherOrders::getOrderId, orderIds));
            for (OtherOrders oo : overdueOrders) {
                long amt = oo.getPayAccount() != null ? oo.getPayAccount() : 0L;
                overduePaidMap.merge(oo.getOrderId(), amt, Long::sum);
            }
        }
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM");
        Map<String, List<Orders>> grouped = orders.stream()
                .collect(Collectors.groupingBy(o -> sdf.format(o.getCreateTime())));
        // ç”Ÿæˆå®Œæ•´å¹´æœˆåˆ—表
        List<String> months = new ArrayList<>();
        Calendar loop = Calendar.getInstance();
        loop.setTime(query.getStartDate());
        loop.set(Calendar.DAY_OF_MONTH, 1);
        Calendar monthEnd = Calendar.getInstance();
        monthEnd.setTime(query.getEndDate());
        while (!loop.after(monthEnd)) {
            months.add(sdf.format(loop.getTime()));
            loop.add(Calendar.MONTH, 1);
        }
        BigDecimal hundred = BigDecimal.valueOf(100);
        List<FinanceOverviewVO> result = new ArrayList<>();
        for (String month : months) {
            List<Orders> monthOrders = grouped.getOrDefault(month, Collections.emptyList());
            long storageRev = 0L, deliveryRev = 0L, shopFee = 0L, driverFeeTotal = 0L, refundTotal = 0L, overdueTotal = 0L;
            for (Orders o : monthOrders) {
                long pay = o.getPayAmount() != null ? o.getPayAmount() : 0L;
                long refund = o.getRefundAmount() != null ? o.getRefundAmount() : 0L;
                long overdue = overduePaidMap.getOrDefault(o.getId(), 0L);
                long actual = pay - refund + overdue;
                long depFee = o.getDepositShopFee() != null ? o.getDepositShopFee() : 0L;
                long takeFee = o.getTakeShopFee() != null ? o.getTakeShopFee() : 0L;
                long dFee = Constants.equalsInteger(o.getType(),Constants.ONE)&&o.getDriverFee() != null ? o.getDriverFee() : 0L;
                if (Constants.equalsInteger(o.getType(), Constants.ZERO)) {
                    storageRev += actual;
                    shopFee += depFee;
                } else {
                    deliveryRev += actual;
                    shopFee += depFee + takeFee;
                }
                driverFeeTotal += dFee;
                refundTotal += refund;
                overdueTotal += overdue;
            }
            long totalRev = storageRev + deliveryRev;
            long netRev = totalRev - shopFee - driverFeeTotal;
            FinanceOverviewVO vo = new FinanceOverviewVO();
            vo.setDate(month);
            vo.setStorageRevenue(BigDecimal.valueOf(storageRev).divide(hundred, 2, RoundingMode.HALF_UP));
            vo.setDeliveryRevenue(BigDecimal.valueOf(deliveryRev).divide(hundred, 2, RoundingMode.HALF_UP));
            vo.setTotalRevenue(BigDecimal.valueOf(totalRev).divide(hundred, 2, RoundingMode.HALF_UP));
            vo.setShopFeeTotal(BigDecimal.valueOf(shopFee).divide(hundred, 2, RoundingMode.HALF_UP));
            vo.setDriverFeeTotal(BigDecimal.valueOf(driverFeeTotal).divide(hundred, 2, RoundingMode.HALF_UP));
            vo.setRefundAmount(BigDecimal.valueOf(refundTotal).divide(hundred, 2, RoundingMode.HALF_UP));
            vo.setOverdueAmount(BigDecimal.valueOf(overdueTotal).divide(hundred, 2, RoundingMode.HALF_UP));
            vo.setNetRevenue(BigDecimal.valueOf(netRev).divide(hundred, 2, RoundingMode.HALF_UP));
            result.add(vo);
        }
        return result;
    }
    @Override
    public List<ShopTopVO> shopTop(TrendQueryDTO query) {
        TrendDateRange range = parseTrendDateRange(query);
        List<Orders> orders = ordersMapper.selectList(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .eq(Orders::getStatus, Constants.OrderStatus.finished.getKey())
                .ge(Orders::getCreateTime, range.startDate)
                .le(Orders::getCreateTime, range.endDate));
        // æ ¹æ®ç­›é€‰å‡ºçš„订单主键查询已支付的逾期费用
        List<Integer> orderIds = orders.stream().map(Orders::getId).collect(Collectors.toList());
        Map<Integer, Long> overduePaidMap = new HashMap<>();
        if (!orderIds.isEmpty()) {
            List<OtherOrders> overdueOrders = otherOrdersMapper.selectList(new QueryWrapper<OtherOrders>().lambda()
                    .eq(OtherOrders::getType, 2)
                    .eq(OtherOrders::getPayStatus, Constants.ONE)
                    .eq(OtherOrders::getDeleted, Constants.ZERO)
                    .in(OtherOrders::getOrderId, orderIds));
            for (OtherOrders oo : overdueOrders) {
                long amt = oo.getPayAccount() != null ? oo.getPayAccount() : 0L;
                overduePaidMap.merge(oo.getOrderId(), amt, Long::sum);
            }
        }
        // æŒ‰é—¨åº—ID归集: key = shopId
        Map<Integer, long[]> map = new LinkedHashMap<>();
        for (Orders o : orders) {
            long pay = o.getPayAmount() != null ? o.getPayAmount() : 0L;
            long refund = o.getRefundAmount() != null ? o.getRefundAmount() : 0L;
            long overdue = overduePaidMap.getOrDefault(o.getId(), 0L);
            boolean hasRefund = refund > 0;
            long rev = pay - refund + overdue;
            // å­˜ä»¶é—¨åº—
            if (o.getDepositShopId() != null) {
                long depFee = o.getDepositShopFee() != null ? o.getDepositShopFee() : 0L;
                long[] arr = map.computeIfAbsent(o.getDepositShopId(), k -> new long[4]); // [count, revenue, fee, refundCount]
                arr[0]++;
                arr[1] += rev;
                arr[2] += depFee;
                if (hasRefund) arr[3]++;
            }
            // å¼‚地且有取件门店
            if (Constants.equalsInteger(o.getType(), Constants.ONE) && o.getTakeShopId() != null) {
                long takeFee = o.getTakeShopFee() != null ? o.getTakeShopFee() : 0L;
                long[] arr = map.computeIfAbsent(o.getTakeShopId(), k -> new long[4]);
                arr[0]++;
                arr[1] += rev;
                arr[2] += takeFee;
                if (hasRefund) arr[3]++;
            }
        }
        // æ‰¹é‡æŸ¥é—¨åº—名称
        Set<Integer> shopIds = map.keySet();
        Map<Integer, String> shopNameMap = new HashMap<>();
        if (!shopIds.isEmpty()) {
            List<ShopInfo> shops = shopInfoMapper.selectList(new QueryWrapper<ShopInfo>().lambda()
                    .in(ShopInfo::getId, shopIds)
                    .select(ShopInfo::getId, ShopInfo::getName));
            for (ShopInfo s : shops) {
                shopNameMap.put(s.getId(), s.getName());
            }
        }
        BigDecimal hundred = BigDecimal.valueOf(100);
        List<ShopTopVO> result = new ArrayList<>();
        for (Map.Entry<Integer, long[]> entry : map.entrySet()) {
            long[] arr = entry.getValue();
            ShopTopVO vo = new ShopTopVO();
            vo.setShopId(entry.getKey());
            vo.setShopName(shopNameMap.getOrDefault(entry.getKey(), ""));
            vo.setCompletedCount(arr[0]);
            vo.setTotalRevenue(BigDecimal.valueOf(arr[1]).divide(hundred, 2, RoundingMode.HALF_UP));
            vo.setShopFeeTotal(BigDecimal.valueOf(arr[2]).divide(hundred, 2, RoundingMode.HALF_UP));
            vo.setRefundCount(arr[3]);
            vo.setPenaltyAmount(BigDecimal.ZERO);
            result.add(vo);
        }
        result.sort((a, b) -> Long.compare(b.getCompletedCount(), a.getCompletedCount()));
        return result.size() > 10 ? result.subList(0, 10) : result;
    }
    @Override
    public List<DriverTopVO> driverTop(TrendQueryDTO query) {
        TrendDateRange range = parseTrendDateRange(query);
        List<Orders> orders = ordersMapper.selectList(new QueryWrapper<Orders>().lambda()
//                .eq(Orders::getDeleted, Constants.ZERO)
                .in(Orders::getStatus,
                        Constants.OrderStatus.waitDeposit.getKey(),
                        Constants.OrderStatus.deposited.getKey(),
                        Constants.OrderStatus.accepted.getKey(),
                        Constants.OrderStatus.delivering.getKey(),
                        Constants.OrderStatus.arrived.getKey(),
                        Constants.OrderStatus.finished.getKey())
                .eq(Orders::getType, Constants.ONE)
                .ge(Orders::getCreateTime, range.startDate)
                .le(Orders::getCreateTime, range.endDate));
        System.out.println(
                orders.stream().map(i->i.getDriverFee()).reduce(0L,Long::sum)
        );
        // æ ¹æ®ç­›é€‰å‡ºçš„订单主键查询已支付的逾期费用
        List<Integer> orderIds = orders.stream().map(Orders::getId).collect(Collectors.toList());
        Map<Integer, Long> overduePaidMap = new HashMap<>();
        if (!orderIds.isEmpty()) {
            List<OtherOrders> overdueOrders = otherOrdersMapper.selectList(new QueryWrapper<OtherOrders>().lambda()
                    .eq(OtherOrders::getType, 2)
                    .eq(OtherOrders::getPayStatus, Constants.ONE)
                    .eq(OtherOrders::getDeleted, Constants.ZERO)
                    .in(OtherOrders::getOrderId, orderIds));
            for (OtherOrders oo : overdueOrders) {
                long amt = oo.getPayAccount() != null ? oo.getPayAccount() : 0L;
                overduePaidMap.merge(oo.getOrderId(), amt, Long::sum);
            }
        }
        // æŒ‰å¸æœºå½’集: key = acceptDriver
        Map<Integer, long[]> map = new LinkedHashMap<>();
        for (Orders o : orders) {
            if (o.getAcceptDriver() == null) continue;
            long pay = o.getPayAmount() != null ? o.getPayAmount() : 0L;
            long refund = o.getRefundAmount() != null ? o.getRefundAmount() : 0L;
            long overdue = overduePaidMap.getOrDefault(o.getId(), 0L);
            long dFee = o.getDriverFee() != null ? o.getDriverFee() : 0L;
            long[] arr = map.computeIfAbsent(o.getAcceptDriver(), k -> new long[4]); // [count, revenue, fee, refundCount]
            arr[0]++;
            arr[1] += pay - refund + overdue;
            arr[2] += dFee;
            if (refund > 0) arr[3]++;
        }
        // æ‰¹é‡æŸ¥å¸æœºå§“名
        Set<Integer> driverIds = map.keySet();
        Map<Integer, String> driverNameMap = new HashMap<>();
        if (!driverIds.isEmpty()) {
            List<DriverInfo> drivers = driverInfoMapper.selectList(new QueryWrapper<DriverInfo>().lambda()
                    .in(DriverInfo::getId, driverIds)
                    .select(DriverInfo::getMemberId, DriverInfo::getName));
            for (DriverInfo d : drivers) {
                driverNameMap.put(d.getMemberId(), d.getName());
            }
        }
        BigDecimal hundred = BigDecimal.valueOf(100);
        List<DriverTopVO> result = new ArrayList<>();
        for (Map.Entry<Integer, long[]> entry : map.entrySet()) {
            long[] arr = entry.getValue();
            DriverTopVO vo = new DriverTopVO();
            vo.setDriverId(entry.getKey());
            vo.setDriverName(driverNameMap.getOrDefault(entry.getKey(), "未知"));
            vo.setCompletedCount(arr[0]);
            vo.setTotalRevenue(BigDecimal.valueOf(arr[1]).divide(hundred, 2, RoundingMode.HALF_UP));
            vo.setDriverFeeTotal(BigDecimal.valueOf(arr[2]).divide(hundred, 2, RoundingMode.HALF_UP));
            vo.setRefundCount(arr[3]);
            vo.setPenaltyAmount(BigDecimal.ZERO);
            result.add(vo);
        }
        result.sort((a, b) -> Long.compare(b.getCompletedCount(), a.getCompletedCount()));
        return result.size() > 10 ? result.subList(0, 10) : result;
    }
    // ========== ç§æœ‰æ–¹æ³• ==========
    private QueryWrapper<Orders> buildOrderQueryWrapper(DataBoardQueryDTO query) {
@@ -294,7 +608,7 @@
                        Constants.OrderStatus.accepted.getKey(),
                        Constants.OrderStatus.delivering.getKey(),
                        Constants.OrderStatus.arrived.getKey(),
                        Constants.OrderStatus.overdue.getKey());
                        Constants.OrderStatus.finished.getKey());
        if (query.getStartDate() != null) {
            qw.lambda().ge(Orders::getCreateTime, query.getStartDate());
        }
@@ -308,30 +622,128 @@
        return qw;
    }
    private Date get30DaysAgoStart() {
        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        cal.add(Calendar.DAY_OF_MONTH, -29);
        return cal.getTime();
    private QueryWrapper<Orders> buildFinanceOrderQueryWrapper(FinanceQueryDTO query) {
        QueryWrapper<Orders> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .in(Orders::getStatus,
                        Constants.OrderStatus.waitDeposit.getKey(),
                        Constants.OrderStatus.deposited.getKey(),
                        Constants.OrderStatus.accepted.getKey(),
                        Constants.OrderStatus.delivering.getKey(),
                        Constants.OrderStatus.arrived.getKey(),
                        Constants.OrderStatus.finished.getKey());
        if (query.getStartDate() != null) {
            qw.lambda().ge(Orders::getCreateTime, query.getStartDate());
        }
        if (query.getEndDate() != null) {
            qw.lambda().le(Orders::getCreateTime, Utils.Date.getEnd(query.getEndDate()));
        }
        return qw;
    }
    private TrendDateRange parseTrendDateRange(TrendQueryDTO query) {
        Calendar now = Calendar.getInstance();
        TrendDateRange range = new TrendDateRange();
        if (StringUtils.isNotBlank(query.getMonth())) {
            // æŒ‰æœˆæŸ¥è¯¢ï¼šyyyy-MM
            SimpleDateFormat parseSdf = new SimpleDateFormat("yyyy-MM");
            Date monthDate;
            try {
                monthDate = parseSdf.parse(query.getMonth());
            } catch (Exception e) {
                throw new IllegalArgumentException("month æ ¼å¼é”™è¯¯ï¼Œåº”为 yyyy-MM");
            }
            Calendar monthCal = Calendar.getInstance();
            monthCal.setTime(monthDate);
            // å½“月1日起始
            monthCal.set(Calendar.DAY_OF_MONTH, 1);
            monthCal.set(Calendar.HOUR_OF_DAY, 0);
            monthCal.set(Calendar.MINUTE, 0);
            monthCal.set(Calendar.SECOND, 0);
            monthCal.set(Calendar.MILLISECOND, 0);
            range.startDate = monthCal.getTime();
            // æœˆæœ«
            monthCal.set(Calendar.DAY_OF_MONTH, monthCal.getActualMaximum(Calendar.DAY_OF_MONTH));
            monthCal.set(Calendar.HOUR_OF_DAY, 23);
            monthCal.set(Calendar.MINUTE, 59);
            monthCal.set(Calendar.SECOND, 59);
            monthCal.set(Calendar.MILLISECOND, 999);
            Date monthEnd = monthCal.getTime();
            // ä¸èƒ½è¶…过当前时间
            range.endDate = monthEnd.after(now.getTime()) ? now.getTime() : monthEnd;
            range.byMonth = true;
            range.pattern = "MM-dd";
        } else if (StringUtils.isNotBlank(query.getYear())) {
            // æŒ‰å¹´æŸ¥è¯¢ï¼šyyyy
            int year = Integer.parseInt(query.getYear());
            Calendar yearStart = Calendar.getInstance();
            yearStart.set(Calendar.YEAR, year);
            yearStart.set(Calendar.MONTH, Calendar.JANUARY);
            yearStart.set(Calendar.DAY_OF_MONTH, 1);
            yearStart.set(Calendar.HOUR_OF_DAY, 0);
            yearStart.set(Calendar.MINUTE, 0);
            yearStart.set(Calendar.SECOND, 0);
            yearStart.set(Calendar.MILLISECOND, 0);
            range.startDate = yearStart.getTime();
            Calendar yearEnd = Calendar.getInstance();
            yearEnd.set(Calendar.YEAR, year);
            yearEnd.set(Calendar.MONTH, Calendar.DECEMBER);
            yearEnd.set(Calendar.DAY_OF_MONTH, 31);
            yearEnd.set(Calendar.HOUR_OF_DAY, 23);
            yearEnd.set(Calendar.MINUTE, 59);
            yearEnd.set(Calendar.SECOND, 59);
            yearEnd.set(Calendar.MILLISECOND, 999);
            Date end = yearEnd.getTime();
            range.endDate = end.after(now.getTime()) ? now.getTime() : end;
            range.byMonth = false;
            range.pattern = "MM";
        } else {
            throw new IllegalArgumentException("请传入 month æˆ– year å‚æ•°");
        }
        return range;
    }
    private static class TrendDateRange {
        Date startDate;
        Date endDate;
        boolean byMonth; // true=按月查(按日返回),false=按年查(按月返回)
        String pattern;  // SimpleDateFormat æ ¼å¼
    }
    @FunctionalInterface
    private interface DailyVOBuilder<T> {
    private interface TrendVOBuilder<T> {
        T build(String date);
    }
    private <T> List<T> buildDailyList(Date startDate, DailyVOBuilder<T> builder) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    private <T> List<T> buildTrendList(TrendDateRange range, TrendVOBuilder<T> builder) {
        List<T> result = new ArrayList<>();
        Calendar loop = Calendar.getInstance();
        loop.setTime(startDate);
        Calendar end = Calendar.getInstance();
        while (!loop.after(end)) {
            result.add(builder.build(sdf.format(loop.getTime())));
            loop.add(Calendar.DAY_OF_MONTH, 1);
        SimpleDateFormat sdf = new SimpleDateFormat(range.pattern);
        if (range.byMonth) {
            // æŒ‰æ—¥å¾ªçޝ
            Calendar loop = Calendar.getInstance();
            loop.setTime(range.startDate);
            Calendar end = Calendar.getInstance();
            end.setTime(range.endDate);
            while (!loop.after(end)) {
                result.add(builder.build(sdf.format(loop.getTime())));
                loop.add(Calendar.DAY_OF_MONTH, 1);
            }
        } else {
            // æŒ‰æœˆå¾ªçޝ 1~12,不能超过 endDate æ‰€åœ¨æœˆ
            Calendar endCal = Calendar.getInstance();
            endCal.setTime(range.endDate);
            int endMonth = endCal.get(Calendar.MONTH); // 0-based
            for (int m = 0; m <= endMonth; m++) {
                String label = String.format("%02d", m + 1);
                result.add(builder.build(label));
            }
        }
        return result;
    }
server/services/src/main/java/com/doumee/service/business/impl/DriverInfoServiceImpl.java
@@ -159,6 +159,10 @@
     * å‘送司机站内信通知
     */
    private void sendDriverNotice(Integer driverId, Constants.DriverOrderNotify notify, Integer orderId, String... params) {
        sendDriverNotice(driverId, notify, orderId, 0, params);
    }
    private void sendDriverNotice(Integer driverId, Constants.DriverOrderNotify notify, Integer objId, Integer objType, String... params) {
        DriverInfo driver = driverInfoMapper.selectById(driverId);
        if (driver == null || driver.getMemberId() == null) {
            return;
@@ -168,8 +172,8 @@
        notice.setUserId(driver.getMemberId());
        notice.setTitle(notify.getTitle());
        notice.setContent(notify.format(params));
        notice.setObjId(orderId);
        notice.setObjType(0); // 0=订单
        notice.setObjId(objId);
        notice.setObjType(objType);
        notice.setStatus(0);  // 0=未读
        notice.setIsdeleted(Constants.ZERO);
        notice.setCreateDate(new Date());
@@ -555,6 +559,10 @@
            newChange.setJpushAlias(official.getJpushAlias());
            driverInfoMapper.insert(newChange);
            saveDriverAttachments(newChange.getId(), request, now);
            // æ ‡è®°åŽ†å²çš„å˜æ›´ç‰ˆæœ¬ä¸ºåˆ é™¤
        } else {
            // auditStatus=0/2:直接更新变更版本
            applyDriverFieldsFromUpdate(changeVersion.getId(), request, now);
@@ -823,6 +831,11 @@
            // çŸ­ä¿¡é€šçŸ¥
            sendDriverAuditSms(changeVersion, newAuditStatus, auditDTO.getAuditRemark());
            // é¦–次审批通过:发送奖励解锁站内信
            if (Constants.equalsInteger(newAuditStatus, Constants.THREE)) {
                sendDriverNotice(changeVersion.getId(), Constants.DriverOrderNotify.REWARD_UNLOCKED, null, 2);
            }
        } else {
            // åœºæ™¯2:正式版本已审批通过(变更审批)
            if (Constants.equalsInteger(newAuditStatus, Constants.THREE)) {
@@ -1797,7 +1810,7 @@
        // 4. æ›´æ–°è®¢å•状态为派送中(4)
        ordersMapper.update(new UpdateWrapper<Orders>().lambda()
                .set(Orders::getStatus, Constants.OrderStatus.delivering.getStatus())
                .set(Orders::getTakeTime, now)
                .set(Orders::getDriverTakeTime, now)
                .set(Orders::getUpdateTime, now)
                .eq(Orders::getId, orderId));
server/services/src/main/java/com/doumee/service/business/impl/InvoiceRecordServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,353 @@
package com.doumee.service.business.impl;
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.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.biz.system.SystemDictDataBiz;
import com.doumee.core.utils.FtpUtil;
import com.doumee.core.utils.Utils;
import com.doumee.dao.business.InvoiceRecordMapper;
import com.doumee.dao.business.OrdersMapper;
import com.doumee.dao.business.model.InvoiceRecord;
import com.doumee.dao.business.model.Orders;
import com.doumee.dao.dto.ApplyInvoiceDTO;
import com.doumee.dao.vo.InvoiceRecordSummaryVO;
import com.doumee.service.business.InvoiceRecordService;
import com.doumee.service.common.EmailService;
import org.apache.commons.lang3.StringUtils;
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.io.File;
import java.io.FileOutputStream;
import java.util.*;
/**
 * å‘票申请记录Service实现
 *
 * @author rk
 * @date 2026/05/18
 */
@Service
public class InvoiceRecordServiceImpl implements InvoiceRecordService {
    @Autowired
    private InvoiceRecordMapper invoiceRecordMapper;
    @Autowired
    private OrdersMapper ordersMapper;
    @Autowired
    private EmailService emailService;
    @Autowired
    private SystemDictDataBiz systemDictDataBiz;
    @Override
    public Integer create(InvoiceRecord invoiceRecord) {
        LoginUserInfo loginUserInfo = (LoginUserInfo) SecurityUtils.getSubject().getPrincipal();
        invoiceRecord.setDeleted(Constants.ZERO);
        invoiceRecord.setCreateTime(new Date());
        invoiceRecord.setCreateUser(loginUserInfo.getId());
        invoiceRecord.setUpdateTime(new Date());
        invoiceRecord.setUpdateUser(loginUserInfo.getId());
        invoiceRecordMapper.insert(invoiceRecord);
        return invoiceRecord.getId();
    }
    @Override
    public void deleteById(Integer id) {
        invoiceRecordMapper.update(new UpdateWrapper<InvoiceRecord>().lambda()
                .set(InvoiceRecord::getDeleted, Constants.ONE)
                .eq(InvoiceRecord::getId, id));
    }
    @Override
    public void delete(InvoiceRecord invoiceRecord) {
        UpdateWrapper<InvoiceRecord> deleteWrapper = new UpdateWrapper<>(invoiceRecord);
        invoiceRecordMapper.delete(deleteWrapper);
    }
    @Override
    public void deleteByIdInBatch(List<Integer> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return;
        }
        for (Integer id : ids) {
            this.deleteById(id);
        }
    }
    @Override
    public void updateById(InvoiceRecord invoiceRecord) {
        LoginUserInfo loginUserInfo = (LoginUserInfo) SecurityUtils.getSubject().getPrincipal();
        invoiceRecord.setUpdateTime(new Date());
        invoiceRecord.setUpdateUser(loginUserInfo.getId());
        invoiceRecordMapper.updateById(invoiceRecord);
    }
    @Override
    public void updateByIdInBatch(List<InvoiceRecord> invoiceRecords) {
        if (CollectionUtils.isEmpty(invoiceRecords)) {
            return;
        }
        for (InvoiceRecord invoiceRecord : invoiceRecords) {
            this.updateById(invoiceRecord);
        }
    }
    @Override
    public InvoiceRecord findById(Integer id) {
        InvoiceRecord invoiceRecord = invoiceRecordMapper.selectById(id);
        if (Objects.isNull(invoiceRecord)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        return invoiceRecord;
    }
    @Override
    public InvoiceRecord findOne(InvoiceRecord invoiceRecord) {
        QueryWrapper<InvoiceRecord> wrapper = new QueryWrapper<>(invoiceRecord);
        return invoiceRecordMapper.selectOne(wrapper);
    }
    @Override
    public List<InvoiceRecord> findList(InvoiceRecord invoiceRecord) {
        QueryWrapper<InvoiceRecord> wrapper = new QueryWrapper<>(invoiceRecord);
        return invoiceRecordMapper.selectList(wrapper);
    }
    @Override
    public PageData<InvoiceRecord> findPage(PageWrap<InvoiceRecord> pageWrap) {
        IPage<InvoiceRecord> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        QueryWrapper<InvoiceRecord> qw = buildAdminQueryWrapper(pageWrap.getModel());
        qw.lambda().orderByDesc(InvoiceRecord::getCreateTime);
        return PageData.from(invoiceRecordMapper.selectPage(page, qw));
    }
    @Override
    public long count(InvoiceRecord invoiceRecord) {
        QueryWrapper<InvoiceRecord> wrapper = new QueryWrapper<>(invoiceRecord);
        return invoiceRecordMapper.selectCount(wrapper);
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void applyInvoice(ApplyInvoiceDTO dto, Integer memberId) {
        // æŸ¥è¯¢è®¢å•
        Orders order = ordersMapper.selectById(dto.getOrderId());
        if (order == null || Constants.equalsInteger(order.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在");
        }
        if (!Constants.equalsInteger(order.getMemberId(), memberId)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "无权操作该订单");
        }
        // æ ¡éªŒå‘票状态:只有1=可申请、99=开具失败可以申请
        if (order.getInvoiceStatus() == null
                || (!Constants.equalsInteger(order.getInvoiceStatus(), Constants.ONE)
                && !Constants.equalsInteger(order.getInvoiceStatus(), 99))) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单不支持开票申请");
        }
        // ä¸ªäºº/事业单位只能开具电子普通发票
        if (Constants.equalsInteger(dto.getOrgType(), Constants.ZERO)
                && !Constants.equalsInteger(dto.getInvoiceType(), Constants.ZERO)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "个人/事业单位只能开具电子普通发票");
        }
        // ä¼ä¸šç±»åž‹ï¼šç¨Žå·å¿…å¡«
        if (Constants.equalsInteger(dto.getOrgType(), Constants.ONE)
                && StringUtils.isBlank(dto.getTaxId())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "企业类型税号不能为空");
        }
        // ç”µå­ä¸“用发票:开户银行、银行账号、企业地址、企业电话必填
        if (Constants.equalsInteger(dto.getInvoiceType(), Constants.ONE)) {
            if (StringUtils.isAnyBlank(dto.getBankName(), dto.getBankAccount(),
                    dto.getCompanyAddr(), dto.getCompanyPhone())) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "电子专用发票需填写开户银行、银行账号、企业地址、企业电话");
            }
        }
        // è®¡ç®—开票金额
        long payAmount = order.getPayAmount() != null ? order.getPayAmount() : 0L;
        long refundAmount = order.getRefundAmount() != null ? order.getRefundAmount() : 0L;
        long invoiceAmount = payAmount - refundAmount;
        // åˆ›å»ºå‘票申请记录
        InvoiceRecord record = new InvoiceRecord();
        record.setOrderId(order.getId());
        record.setOrderNo(order.getCode());
        record.setMemberId(memberId);
        record.setOrgType(dto.getOrgType());
        record.setInvoiceType(dto.getInvoiceType());
        record.setName(dto.getName());
        record.setTaxId(dto.getTaxId());
        record.setBankName(dto.getBankName());
        record.setBankAccount(dto.getBankAccount());
        record.setCompanyAddr(dto.getCompanyAddr());
        record.setCompanyPhone(dto.getCompanyPhone());
        record.setInvoiceAmount(invoiceAmount);
        record.setStatus(Constants.ZERO); // 0=申请中
        record.setDeleted(Constants.ZERO);
        record.setCreateTime(new Date());
        invoiceRecordMapper.insert(record);
        // æ›´æ–°è®¢å•发票状态为申请中
        order.setInvoiceStatus(Constants.TWO);
        order.setUpdateTime(new Date());
        ordersMapper.updateById(order);
    }
    @Override
    public PageData<InvoiceRecord> findMemberInvoicePage(PageWrap<InvoiceRecord> pageWrap, Integer memberId) {
        IPage<InvoiceRecord> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        QueryWrapper<InvoiceRecord> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(InvoiceRecord::getDeleted, Constants.ZERO)
                .eq(InvoiceRecord::getMemberId, memberId)
//                .eq(InvoiceRecord::getStatus, Constants.ONE) // åªæŸ¥å·²å¼€å…·æˆåŠŸ
                .orderByDesc(InvoiceRecord::getCreateTime);
        return PageData.from(invoiceRecordMapper.selectPage(page, qw));
    }
    @Override
    public void sendInvoiceEmail(Integer memberId, Integer invoiceRecordId, String email) {
        InvoiceRecord record = invoiceRecordMapper.selectById(invoiceRecordId);
        if (record == null || Constants.equalsInteger(record.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "发票记录不存在");
        }
        if (!Constants.equalsInteger(record.getMemberId(), memberId)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "无权操作该发票记录");
        }
        if (!Constants.equalsInteger(record.getStatus(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "发票尚未开具成功");
        }
        if (StringUtils.isBlank(record.getFileAddr())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "发票文件不存在");
        }
        // æ‹¼æŽ¥æ–‡ä»¶å®Œæ•´URL
        String fullUrl = record.getFileAddr();
        if (!fullUrl.startsWith("http")) {
            try {
                String prefix = systemDictDataBiz.queryByCode(Constants.OSS, Constants.RESOURCE_PATH).getCode();
                fullUrl = prefix + fullUrl;
            } catch (Exception e) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "文件地址配置缺失");
            }
        }
        // ä¸‹è½½å‘票文件
        byte[] fileBytes;
        try {
            fileBytes = new FtpUtil().getOnlineInputsteam(fullUrl);
        } catch (Exception e) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "发票文件下载失败");
        }
        if (fileBytes == null || fileBytes.length == 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "发票文件为空");
        }
        // å†™å…¥ä¸´æ—¶æ–‡ä»¶
        File tempFile = null;
        try {
            tempFile = File.createTempFile("invoice_", ".pdf");
            try (FileOutputStream fos = new FileOutputStream(tempFile)) {
                fos.write(fileBytes);
            }
            // æž„建附件列表
            String fileName = "发票_" + record.getOrderNo() + ".pdf";
            Map<String, Object> attachment = new HashMap<>();
            attachment.put("name", fileName);
            attachment.put("file", tempFile);
            boolean success = emailService.sendWithAttachment(email, "您的电子发票", "您好,您的发票详见附件,请查收。", Collections.singletonList(attachment));
            if (!success) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "邮件发送失败");
            }
        } catch (BusinessException e) {
            throw e;
        } catch (Exception e) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "邮件发送异常");
        } finally {
            if (tempFile != null && tempFile.exists()) {
                tempFile.delete();
            }
        }
    }
    /**
     * æž„建 admin ç«¯å‘票查询条件(findPage å’Œ summary å…±ç”¨ï¼‰
     */
    private QueryWrapper<InvoiceRecord> buildAdminQueryWrapper(InvoiceRecord model) {
        Utils.MP.blankToNull(model);
        model.setDeleted(Constants.ZERO);
        QueryWrapper<InvoiceRecord> qw = new QueryWrapper<>();
        qw.lambda().eq(InvoiceRecord::getDeleted, Constants.ZERO);
        if (model.getInvoiceNo() != null && StringUtils.isNotBlank(model.getInvoiceNo())) {
            qw.lambda().like(InvoiceRecord::getInvoiceNo, model.getInvoiceNo());
        }
        if (model.getInvoiceType() != null) {
            qw.lambda().eq(InvoiceRecord::getInvoiceType, model.getInvoiceType());
        }
        if (model.getMemberId() != null) {
            qw.lambda().eq(InvoiceRecord::getMemberId, model.getMemberId());
        }
        if (model.getOrgType() != null) {
            qw.lambda().eq(InvoiceRecord::getOrgType, model.getOrgType());
        }
        if (model.getName() != null && StringUtils.isNotBlank(model.getName())) {
            qw.lambda().like(InvoiceRecord::getName, model.getName());
        }
        if (model.getStatus() != null) {
            qw.lambda().eq(InvoiceRecord::getStatus, model.getStatus());
        }
        if (model.getStartDate() != null) {
            qw.lambda().ge(InvoiceRecord::getCreateTime, model.getStartDate());
        }
        if (model.getEndDate() != null) {
            qw.lambda().le(InvoiceRecord::getCreateTime, Utils.Date.getEnd(model.getEndDate()));
        }
        return qw;
    }
    @Override
    public InvoiceRecordSummaryVO findPageSummary(PageWrap<InvoiceRecord> pageWrap) {
        QueryWrapper<InvoiceRecord> baseQw = buildAdminQueryWrapper(pageWrap.getModel());
        // å¼€ç¥¨æ€»é¢ï¼šç¬¦åˆæ¡ä»¶çš„æ‰€æœ‰è®°å½•
        QueryWrapper<InvoiceRecord> totalQw = baseQw.clone();
        totalQw.select("IFNULL(SUM(INVOICE_AMOUNT), 0) as invoiceAmount");
        Map<String, Object> totalResult = invoiceRecordMapper.selectMaps(totalQw).stream().findFirst().orElse(null);
        long totalAmount = totalResult != null && totalResult.get("invoiceAmount") != null
                ? Long.parseLong(totalResult.get("invoiceAmount").toString()) : 0L;
        // å·²å¼€ç¥¨æ€»é¢ï¼šstatus=1
        QueryWrapper<InvoiceRecord> issuedQw = baseQw.clone();
        issuedQw.eq("STATUS", Constants.ONE);
        issuedQw.select("IFNULL(SUM(INVOICE_AMOUNT), 0) as invoiceAmount");
        Map<String, Object> issuedResult = invoiceRecordMapper.selectMaps(issuedQw).stream().findFirst().orElse(null);
        long issuedAmount = issuedResult != null && issuedResult.get("invoiceAmount") != null
                ? Long.parseLong(issuedResult.get("invoiceAmount").toString()) : 0L;
        InvoiceRecordSummaryVO vo = new InvoiceRecordSummaryVO();
        vo.setTotalAmount(totalAmount);
        vo.setIssuedAmount(issuedAmount);
        return vo;
    }
}
server/services/src/main/java/com/doumee/service/business/impl/MemberCouponServiceImpl.java
@@ -11,7 +11,9 @@
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.core.utils.Utils;
import com.doumee.dao.business.CouponMapper;
import com.doumee.dao.business.MemberCouponMapper;
import com.doumee.dao.business.model.Coupon;
import com.doumee.dao.business.model.MemberCoupon;
import com.doumee.dao.system.model.SystemUser;
import com.doumee.service.business.MemberCouponService;
@@ -22,6 +24,7 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Objects;
@@ -31,6 +34,8 @@
    @Autowired
    private MemberCouponMapper memberCouponMapper;
    @Autowired
    private CouponMapper couponMapper;
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
@@ -146,4 +151,79 @@
        QueryWrapper<MemberCoupon> wrapper = new QueryWrapper<>(memberCoupon);
        return memberCouponMapper.selectCount(wrapper);
    }
    @Override
    public PageData<MemberCoupon> findMemberPage(Integer memberId, Integer status, PageWrap<MemberCoupon> pageWrap) {
        IPage<MemberCoupon> page = new Page<>(pageWrap.getPage(), pageWrap.getCapacity());
        MPJLambdaWrapper<MemberCoupon> qw = new MPJLambdaWrapper<>();
        qw.selectAll(MemberCoupon.class)
                .eq(MemberCoupon::getMemberId, memberId)
                .eq(MemberCoupon::getIsdeleted, Constants.ZERO);
        if (status != null) {
            if (status == Constants.CouponStatus.expired.getKey()) {
                // å·²å¤±æ•ˆåŒ…含两种:98=未领取已过期, 99=已过期
                qw.in(MemberCoupon::getStatus,
                        Constants.CouponStatus.expiredUnclaimed.getKey(),
                        Constants.CouponStatus.expired.getKey());
            } else {
                qw.eq(MemberCoupon::getStatus, status);
            }
        }
        // æŒ‰çŠ¶æ€æŽ’åº
        if (status != null) {
            switch (status) {
                case 0: // å¾…领取 â†’ åˆ›å»ºæ—¶é—´å€’序
                case 98: // å·²å¤±æ•ˆ(查询时包含了98和99)
                case 99:
                    qw.orderByDesc(MemberCoupon::getCreateDate);
                    break;
                case 1: // å¾…使用 â†’ é¢†å–时间倒序
                    qw.orderByDesc(MemberCoupon::getValidDate);
                    break;
                case 2: // å·²ä½¿ç”¨ â†’ ä½¿ç”¨æ—¶é—´å€’序
                    qw.orderByDesc(MemberCoupon::getUseDate);
                    break;
                default:
                    qw.orderByDesc(MemberCoupon::getId);
                    break;
            }
        } else {
            qw.orderByDesc(MemberCoupon::getId);
        }
        return PageData.from(memberCouponMapper.selectJoinPage(page, MemberCoupon.class, qw));
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void claimCoupon(Integer memberId, Integer couponId) {
        // æŸ¥è¯¢è¯¥ä¼šå‘˜çš„待领取优惠券记录
        MemberCoupon mc = memberCouponMapper.selectOne(new QueryWrapper<MemberCoupon>().lambda()
                .eq(MemberCoupon::getId, couponId)
                .eq(MemberCoupon::getMemberId, memberId)
                .eq(MemberCoupon::getStatus, Constants.CouponStatus.waitClaim.getKey())
                .eq(MemberCoupon::getIsdeleted, Constants.ZERO));
        if (mc == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        // æ ¡éªŒä¼˜æƒ åˆ¸æ˜¯å¦æœ‰æ•ˆ
        Coupon coupon = couponMapper.selectById(couponId);
        if (coupon == null || coupon.getStatus() != Constants.ZERO) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "优惠券无效");
        }
        // æ ‡è®°å·²é¢†å–,计算有效期
        Date now = new Date();
        Calendar cal = Calendar.getInstance();
        cal.setTime(now);
        cal.add(Calendar.DAY_OF_MONTH, mc.getValidDays() != null ? mc.getValidDays() : 0);
        memberCouponMapper.update(new UpdateWrapper<MemberCoupon>().lambda()
                .set(MemberCoupon::getStatus, Constants.CouponStatus.claimed.getKey())
                .set(MemberCoupon::getValidDate, now)
                .set(MemberCoupon::getStartDate, now)
                .set(MemberCoupon::getEndDate, cal.getTime())
                .set(MemberCoupon::getEditDate, now)
                .eq(MemberCoupon::getId, mc.getId()));
    }
}
server/services/src/main/java/com/doumee/service/business/impl/MemberServiceImpl.java
@@ -356,6 +356,8 @@
                member.setAutoReceiveStatus(Constants.ONE);
                member.setUserType(Constants.ZERO);
                memberMapper.insert(member);
                // æ³¨å†Œèµ é€ä¼˜æƒ åˆ¸
                giftRegisterCoupons(member);
            }
            String token = JwtTokenUtil.generateTokenForRedis(member.getId(), Constants.ZERO, JSONObject.toJSONString(member), redisTemplate);
            AccountResponse accountResponse = new AccountResponse();
@@ -426,6 +428,7 @@
            || ( StringUtils.isBlank(member.getCoverImage())
                && StringUtils.isBlank(member.getName())
                && StringUtils.isBlank(member.getNickName())
                && StringUtils.isBlank(member.getEmail())
                && Objects.isNull(member.getAutoReceiveStatus()) )){
            throw new BusinessException(ResponseStatus.BAD_REQUEST);
        }
@@ -433,6 +436,7 @@
                .set(StringUtils.isNotBlank(member.getNickName()),Member::getNickName,member.getNickName())
                .set(StringUtils.isNotBlank(member.getName()),Member::getName,member.getName())
                .set(StringUtils.isNotBlank(member.getCoverImage()),Member::getCoverImage,member.getCoverImage())
                .set(StringUtils.isNotBlank(member.getEmail()),Member::getEmail,member.getEmail())
                .set(Objects.nonNull(member.getAutoReceiveStatus()),Member::getAutoReceiveStatus,member.getAutoReceiveStatus())
                        .set(Member::getUpdateTime,new Date())
                .eq(Member::getId,member.getId()));
@@ -452,6 +456,7 @@
        userCenterVO.setTelephone(member.getTelephone());
        userCenterVO.setCoverImage(member.getCoverImage());
        userCenterVO.setOpenid(member.getOpenid());
        userCenterVO.setEmail(member.getEmail());
        if(StringUtils.isNotBlank(member.getCoverImage())){
            String path  = systemDictDataBiz.queryByCode(Constants.OSS,Constants.RESOURCE_PATH).getCode()
                    +systemDictDataBiz.queryByCode(Constants.OSS,Constants.MEMBER_FILES).getCode();
@@ -803,4 +808,39 @@
        log.info("注册满年赠送优惠券完成,共处理{}名会员", giftedMemberCount);
    }
    private void giftRegisterCoupons(Member member) {
        String couponIdsStr = operationConfigBiz.getConfig().getRegisterGiftCouponIds();
        if (StringUtils.isBlank(couponIdsStr)) {
            return;
        }
        String[] idArr = couponIdsStr.split(",");
        Date now = new Date();
        for (String idStr : idArr) {
            String trimmed = idStr.trim();
            if (StringUtils.isBlank(trimmed)) continue;
            Coupon coupon = couponMapper.selectById(Integer.valueOf(trimmed));
            if (coupon == null || Constants.equalsInteger(coupon.getIsdeleted(), Constants.ONE)) continue;
            MemberCoupon mc = new MemberCoupon();
            mc.setCouponId(coupon.getId());
            mc.setMemberId(member.getId());
            mc.setStatus(Constants.CouponStatus.waitClaim.getKey());
            Calendar validCal = Calendar.getInstance();
            validCal.add(Calendar.DAY_OF_MONTH, coupon.getPushDays() != null ? coupon.getPushDays() : 7);
            mc.setValidDate(validCal.getTime());
            mc.setName(coupon.getName());
            mc.setInfo(coupon.getInfo());
            mc.setType(coupon.getType());
            mc.setLimitPrice(coupon.getLimitPrice());
            mc.setPrice(coupon.getPrice());
            mc.setGetMethod(coupon.getGetMethod());
            mc.setCouponType(coupon.getCouponType());
            mc.setPushDays(coupon.getPushDays());
            mc.setValidDays(coupon.getValidDays());
            mc.setIsdeleted(Constants.ZERO);
            mc.setCreateDate(now);
            mc.setEditDate(now);
            memberCouponMapper.insert(mc);
        }
    }
}
server/services/src/main/java/com/doumee/service/business/impl/OrdersRefundServiceImpl.java
@@ -149,7 +149,7 @@
                .select("o.type as orderType")
                .select("o.good_level as goodLevel")
                .select("o.total_amount as totalAmount")
                .select("o.pay_amount as payAmount")
                .select("(o.pay_amount + ifnull(o.OVERDUE_AMOUNT,0)) as payAmount")
                .select("c2.name as goodLevelName")
                .innerJoin("orders o on o.id = t.ORDER_ID ")
                .leftJoin("category c1 on c1.id = o.GOOD_TYPE and c1.DELETED = 0")
server/services/src/main/java/com/doumee/service/business/impl/OrdersServiceImpl.java
@@ -36,6 +36,8 @@
import com.doumee.dao.dto.CommentOrderDTO;
import com.doumee.dao.dto.CreateOrderDTO;
import com.doumee.dao.dto.DispatchDTO;
import com.doumee.dao.dto.HandleOrderExceptionDTO;
import com.doumee.dao.dto.ManualRefundDTO;
import com.doumee.dao.dto.MyOrderDTO;
import com.doumee.dao.dto.OrderItemDTO;
import com.doumee.dao.vo.*;
@@ -112,6 +114,9 @@
    @Autowired
    private RevenueMapper revenueMapper;
    @Autowired
    private RewardRecordMapper rewardRecordMapper;
    @Autowired
@@ -125,6 +130,9 @@
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private MemberCouponMapper memberCouponMapper;
    @Autowired
    private AreasBiz areasBiz;
@@ -229,7 +237,7 @@
                .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");
                .leftJoin("shop_info s2 on s2.id = t.TAKE_SHOP_ID") ;
                ;
        Utils.MP.blankToNull(pageWrap.getModel());
        queryWrapper.eq(pageWrap.getModel().getDeleted() != null, Orders::getDeleted, pageWrap.getModel().getDeleted());
@@ -259,6 +267,11 @@
            if(Constants.equalsInteger(o.getIsUrgent(),Constants.ZERO)){
                o.setUrgentAmount(Constants.ZERO.longValue());
            }
            // å®žä»˜é‡‘额 = æ”¯ä»˜é‡‘额 - é€€æ¬¾é‡‘额 + é€¾æœŸè´¹ç”¨
            long pay = o.getPayAmount() != null ? o.getPayAmount() : 0L;
            long refund = o.getRefundAmount() != null ? o.getRefundAmount() : 0L;
            long overdue = o.getOverdueAmount() != null ? o.getOverdueAmount() : 0L;
            o.setPayAmount(pay - refund + overdue);
        }
        return pageData;
    }
@@ -427,6 +440,7 @@
        result.setTotalPrice(totalPrice);
        result.setDays(days);
        result.setUrgentFee(0L);
        resolveCoupon(result, dto.getMemberId(), dto.getCouponId(), totalPrice);
        return result;
    }
@@ -630,7 +644,50 @@
            }
        }
        resolveCoupon(result, dto.getMemberId(), dto.getCouponId(), totalPrice);
        return result;
    }
    private void resolveCoupon(PriceCalculateVO result, Integer memberId, Integer couponId, long totalPrice) {
        if (memberId == null) {
            result.setDeductionAmount(0L);
            return;
        }
        // æŸ¥è¯¢å¯ç”¨ä¼˜æƒ åˆ¸ï¼šå·²é¢†å–、未过期、满足满额门槛
        Date now = new Date();
        List<MemberCoupon> availableCoupons = memberCouponMapper.selectList(new QueryWrapper<MemberCoupon>().lambda()
                .eq(MemberCoupon::getMemberId, memberId)
                .eq(MemberCoupon::getStatus, Constants.CouponStatus.claimed.getKey())
                .eq(MemberCoupon::getIsdeleted, Constants.ZERO)
                .le(MemberCoupon::getLimitPrice, totalPrice)
                .ge(MemberCoupon::getEndDate, now)
                .orderByDesc(MemberCoupon::getPrice)
                .orderByAsc(MemberCoupon::getEndDate));
        result.setAvailableCoupons(availableCoupons);
        if (couponId == null) {
            result.setDeductionAmount(0L);
            return;
        }
        MemberCoupon selected;
        if (couponId == -1) {
            selected = availableCoupons.isEmpty() ? null : availableCoupons.get(0);
        } else {
            selected = availableCoupons.stream()
                    .filter(mc -> mc.getId().equals(couponId))
                    .findFirst().orElse(null);
            if (selected == null) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "优惠券无效或不可用");
            }
        }
        if (selected != null) {
            result.setDeductionAmount(selected.getPrice());
            result.setSelectedCoupon(selected);
        } else {
            result.setDeductionAmount(0L);
        }
    }
    @Override
@@ -767,6 +824,8 @@
            priceDTO.setDepositEndTime(takeTime);
            priceDTO.setItems(dto.getItems());
            priceDTO.setDeclaredAmount(dto.getDeclaredAmount());
            priceDTO.setMemberId(memberId);
            priceDTO.setCouponId(dto.getCouponId());
            priceResult = calculateLocalPrice(priceDTO);
        } else {
            // å¼‚地寄存
@@ -779,6 +838,8 @@
            priceDTO.setItems(dto.getItems());
            priceDTO.setDeclaredAmount(dto.getDeclaredAmount());
            priceDTO.setUrgent(Constants.ONE.equals(dto.getIsUrgent()));
            priceDTO.setMemberId(memberId);
            priceDTO.setCouponId(dto.getCouponId());
            priceResult = calculateRemotePrice(priceDTO);
        }
@@ -867,8 +928,12 @@
        // è´¹ç”¨ä¿¡æ¯(分)
        orders.setBasicAmount(priceResult.getItemPrice());
        orders.setEstimatedAmount(priceResult.getTotalPrice());
        orders.setTotalAmount(priceResult.getTotalPrice());
        long deductionAmount = priceResult.getDeductionAmount() != null ? priceResult.getDeductionAmount() : 0L;
        orders.setTotalAmount(priceResult.getTotalPrice() - deductionAmount);
        orders.setUrgentAmount(priceResult.getUrgentFee());
        orders.setCouponId(dto.getCouponId());
        orders.setDeductionAmount(deductionAmount);
        orders.setManualRefund(Constants.ZERO);
        // å­˜å‚¨åŠ æ€¥ç³»æ•°
        if (Constants.ONE.equals(dto.getType()) && Constants.ONE.equals(dto.getIsUrgent())) {
            String urgentRateStr = systemDictDataBiz.queryByCode(
@@ -891,6 +956,16 @@
        ordersMapper.insert(orders);
        Integer orderId = orders.getId();
        // æ ‡è®°ä¼˜æƒ åˆ¸å·²ä½¿ç”¨
        MemberCoupon selectedCoupon = priceResult.getSelectedCoupon();
        if (selectedCoupon != null) {
            memberCouponMapper.update(new UpdateWrapper<MemberCoupon>().lambda()
                    .set(MemberCoupon::getStatus, Constants.CouponStatus.used.getKey())
                    .set(MemberCoupon::getUseDate, new Date())
                    .set(MemberCoupon::getOrderId, orderId)
                    .eq(MemberCoupon::getId, selectedCoupon.getId()));
        }
        // åˆ›å»ºè®¢å•日志
        OrderLog createLog = new OrderLog();
@@ -1079,6 +1154,7 @@
        OrderDetailVO vo = new OrderDetailVO();
        vo.setOrder(order);
        vo.setDeductionAmount(order.getDeductionAmount());
        // è®¢å•状态描述
        if (order.getStatus() != null) {
@@ -1553,6 +1629,7 @@
                .select("s1.name", Orders::getDepositShopName)
                .select("s1.link_name", Orders::getDepositShopLinkName)
                .select("s1.link_phone", Orders::getDepositShopLinkPhone)
                .select("s1.address", Orders::getDepositShopAddress)
                .select("s2.name", Orders::getTakeShopName)
                .select("s2.address", Orders::getTakeShopAddress)
                .select("s2.link_phone", Orders::getTakeShopLinkPhone)
@@ -1566,6 +1643,21 @@
                .eq(status != null, Orders::getStatus, status)
                .in(statusList != null, Orders::getStatus, statusList)
                .orderByDesc(Orders::getCreateTime);
        // å¯å¼€ç¥¨è®¢å•:增加月份限制
        if (model != null && model.getInvoiceStatus() != null && Constants.equalsInteger(model.getInvoiceStatus(), Constants.ONE)) {
            wrapper.in(Orders::getInvoiceStatus, Arrays.asList(Constants.ONE, 99));
            String monthLimitStr = operationConfigBiz.getConfig().getInvoiceMonthLimit();
            if (StringUtils.isNotBlank(monthLimitStr)) {
                int monthLimit = Integer.parseInt(monthLimitStr);
                Calendar cal = Calendar.getInstance();
                cal.add(Calendar.MONTH, -monthLimit);
                cal.set(Calendar.HOUR_OF_DAY, 0);
                cal.set(Calendar.MINUTE, 0);
                cal.set(Calendar.SECOND, 0);
                cal.set(Calendar.MILLISECOND, 0);
                wrapper.ge(Orders::getFinishTime, cal.getTime());
            }
        }
        // å…³é”®è¯æœç´¢ï¼šæ”¶ä»¶äºº/收件人电话模糊、订单号精准
        if (model != null && StringUtils.isNotBlank(model.getKeyword())) {
            String kw = model.getKeyword().trim();
@@ -1601,7 +1693,7 @@
                vo.setDepositShopName(o.getDepositShopName());
                vo.setDepositShopLinkName(o.getDepositShopLinkName());
                vo.setDepositShopPhone(o.getDepositShopLinkPhone());
                vo.setDepositShopAddress(o.getDepositShopAddress());
                // å–件信息:有取件门店取门店,无则取用户自选取件点
                if (o.getTakeShopId() != null) {
                    vo.setTakeShopId(o.getTakeShopId());
@@ -1643,6 +1735,14 @@
                vo.setOrderImages(getFileUrlsFromList(o.getId(),files));
                // é€¾æœŸçŠ¶æ€
                fillOverdueStatus(vo, o, details);
                // ä¼˜æƒ åˆ¸æŠµæ‰£é‡‘额
                vo.setDeductionAmount(o.getDeductionAmount());
                // å¯å¼€ç¥¨é‡‘额(支付金额 - é€€æ¬¾é‡‘额)
                if (model != null && model.getInvoiceStatus() != null && Constants.equalsInteger(model.getInvoiceStatus(), Constants.ONE)) {
                    long payAmt = o.getPayAmount() != null ? o.getPayAmount() : 0L;
                    long refundAmt = o.getRefundAmount() != null ? o.getRefundAmount() : 0L;
                    vo.setInvoiceAmount(payAmt - refundAmt);
                }
                voList.add(vo);
            }
        }
@@ -1890,7 +1990,10 @@
        //序号
        vo.setSortnum(Constants.formatIntegerNum(order.getDepositShopId())+"-"+order.getId());
        if(order.getTakeShopId()!=null){
            vo.setSortnumTake(Constants.formatIntegerNum(order.getTakeShopId())+"-"+order.getId());
            String dateStr = new SimpleDateFormat("dd").format(order.getPayTime() != null ? order.getPayTime() : new Date());
            String autoNumStr = String.format("%03d", order.getAutoNum() != null ? order.getAutoNum() : 0);
            String sort = order.getTakeShopId() + "-" + dateStr + "-" + autoNumStr;
            vo.setSortnumTake(sort);
        }
        // å­˜ä»¶é—¨åº—
        if (order.getDepositShopId() != null) {
@@ -2044,6 +2147,7 @@
            order.setStatus(Constants.OrderStatus.cancelled.getStatus());
            order.setCancelTime(now);
            ordersMapper.updateById(order);
            restoreCoupon(order);
            saveCancelLog(order, Constants.OrderLogType.memberCancel, "会员取消订单(待支付)", memberId);
            // çŸ­ä¿¡é€šçŸ¥ä¼šå‘˜ï¼šè®¢å•已取消
            Member cancelMember1 = memberMapper.selectById(memberId);
@@ -2105,6 +2209,7 @@
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "退款失败,请联系客服处理");
            }
            ordersRefundMapper.insert(refund);
            restoreCoupon(order);
            return;
        }
@@ -2176,6 +2281,19 @@
    /**
     * ä¿å­˜å–消订单操作日志
     */
    private void restoreCoupon(Orders order) {
//        if (order.getCouponId() == null || order.getDeductionAmount() == null || order.getDeductionAmount() <= 0) {
//            return;
//        }
//        memberCouponMapper.update(new UpdateWrapper<MemberCoupon>().lambda()
//                .set(MemberCoupon::getStatus, Constants.CouponStatus.claimed.getKey())
//                .set(MemberCoupon::getUseDate, null)
//                .set(MemberCoupon::getOrderId, null)
//                .eq(MemberCoupon::getId, order.getCouponId())
//                .eq(MemberCoupon::getOrderId, order.getId())
//                .eq(MemberCoupon::getStatus, Constants.CouponStatus.used.getKey()));
    }
    private void saveCancelLog(Orders order, Constants.OrderLogType logType, String reason, Integer memberId) {
        OrderLog log = new OrderLog();
        log.setOrderId(order.getId());
@@ -2188,6 +2306,217 @@
        log.setCreateTime(new Date());
        log.setDeleted(Constants.ZERO);
        orderLogService.create(log);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void manualRefund(ManualRefundDTO dto, Integer userId) {
        // 1. æ ¡éªŒè®¢å•
        Orders order = ordersMapper.selectById(dto.getOrderId());
        if (order == null || Constants.equalsInteger(order.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY);
        }
        if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.finished.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅已完成订单可手动退款");
        }
        if (Constants.equalsInteger(order.getManualRefund(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单已手动退款");
        }
        // å¼‚常订单不允许手动退款
        if (Constants.equalsInteger(order.getExceptionStatus(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "异常订单不支持手动退款");
        }
        // 2. æ ¡éªŒé€€æ¬¾é‡‘额
        long payAmount = order.getPayAmount() != null ? order.getPayAmount() : 0L;
        long existingRefund = order.getRefundAmount() != null ? order.getRefundAmount() : 0L;
        long maxRefund = payAmount - existingRefund;
        if (dto.getRefundAmount() <= 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "退款金额必须大于0");
        }
        if (dto.getRefundAmount() > maxRefund) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "退款金额不能超过可退金额(" + maxRefund + "分)");
        }
        // 3. æ ¡éªŒå¹¶æ¸…理扣款金额(根据订单类型)
        boolean isRemote = Constants.equalsInteger(order.getType(), Constants.ONE);
        Long depositShopDeduct = dto.getDepositShopDeduct();
        Long takeShopDeduct = dto.getTakeShopDeduct();
        Long driverDeduct = dto.getDriverDeduct();
        if (!isRemote) {
            takeShopDeduct = null;
            driverDeduct = null;
        } else {
            if (order.getTakeShopId() == null) {
                takeShopDeduct = null;
            }
        }
        if (depositShopDeduct != null && depositShopDeduct < 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "存件门店扣款金额不能为负");
        }
        if (takeShopDeduct != null && takeShopDeduct < 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "取件门店扣款金额不能为负");
        }
        if (driverDeduct != null && driverDeduct < 0) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "司机扣款金额不能为负");
        }
        // 4. æž„建 deductInfo JSON
        String deductInfo = null;
        JSONObject deductJson = new JSONObject();
        if (depositShopDeduct != null) {
            deductJson.put("depositShopDeduct", depositShopDeduct);
        }
        if (takeShopDeduct != null) {
            deductJson.put("takeShopDeduct", takeShopDeduct);
        }
        if (driverDeduct != null) {
            deductJson.put("driverDeduct", driverDeduct);
        }
        if (deductJson.size() > 0) {
            deductInfo = deductJson.toJSONString();
        }
        // 5. åˆ›å»ºé€€æ¬¾è®°å½•
        Date now = new Date();
        String outRefundNo = ID.nextGUID();
        OrdersRefund refund = new OrdersRefund();
        refund.setOrderId(order.getId());
        refund.setType(Constants.FOUR); // æ‰‹åŠ¨é€€æ¬¾
        refund.setRefundAmount(dto.getRefundAmount());
        refund.setDeductInfo(deductInfo);
        refund.setRefundRemark(dto.getRemark());
        refund.setUserId(userId);
        refund.setBeforeStatus(order.getStatus());
        refund.setRefundCode(outRefundNo);
        refund.setCreateTime(now);
        refund.setDeleted(Constants.ZERO);
        ordersRefundMapper.insert(refund);
        // 6. è°ƒç”¨å¾®ä¿¡é€€æ¬¾
        try {
            Refund refundResult = wxPayV3Service.refund(
                    outRefundNo, order.getOutTradeNo(), payAmount, dto.getRefundAmount(),
                    "手动退款", wxPayProperties.getV3RefundNotifyUrl());
            com.wechat.pay.java.service.refund.model.Status refundStatus = refundResult.getStatus();
            if (com.wechat.pay.java.service.refund.model.Status.SUCCESS.equals(refundStatus)) {
                refund.setStatus(Constants.ONE);
                refund.setRefundTime(now);
                ordersRefundMapper.updateById(refund);
                // é€€æ¬¾æˆåŠŸï¼Œæ‰§è¡Œæ‰£æ¬¾
                processManualRefundDeduction(order, depositShopDeduct, takeShopDeduct, driverDeduct);
            } else if (com.wechat.pay.java.service.refund.model.Status.PROCESSING.equals(refundStatus)) {
                refund.setStatus(Constants.ZERO); // é€€æ¬¾ä¸­ï¼Œç­‰å›žè°ƒ
                ordersRefundMapper.updateById(refund);
            } else {
                refund.setStatus(Constants.TWO);
                refund.setRefundRemark("微信退款失败: " + refundStatus.name());
                ordersRefundMapper.updateById(refund);
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "退款失败,请联系客服处理");
            }
        } catch (BusinessException e) {
            throw e;
        } catch (Exception e) {
            log.error("手动退款调用微信退款异常, orderId={}", order.getId(), e);
            refund.setStatus(Constants.TWO);
            ordersRefundMapper.updateById(refund);
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "退款调用异常,请稍后重试");
        }
        // 7. æ›´æ–°è®¢å•:标记已手动退款,累加退款金额
        ordersMapper.update(new UpdateWrapper<Orders>().lambda()
                .set(Orders::getManualRefund, Constants.ONE)
                .setSql(" REFUND_AMOUNT = IFNULL(REFUND_AMOUNT, 0) + " + dto.getRefundAmount())
                .set(Orders::getUpdateTime, now)
                .eq(Orders::getId, order.getId()));
    }
    /**
     * æ‰‹åŠ¨é€€æ¬¾æˆåŠŸåŽï¼Œæ ¹æ®æ‰£æ¬¾ä¿¡æ¯æ‰£é™¤å¯¹åº”æ–¹ä½™é¢å¹¶ç”Ÿæˆæ”¶æ”¯è®°å½•
     */
    private void processManualRefundDeduction(Orders order, Long depositShopDeduct, Long takeShopDeduct, Long driverDeduct) {
        Date now = new Date();
        // å­˜ä»¶é—¨åº—扣款
        if (depositShopDeduct != null && depositShopDeduct > 0 && order.getDepositShopId() != null) {
            ShopInfo depositShop = shopInfoMapper.selectById(order.getDepositShopId());
            if (depositShop != null) {
                shopInfoMapper.update(new UpdateWrapper<ShopInfo>().lambda()
                        .setSql(" BALANCE = IFNULL(BALANCE, 0) - " + depositShopDeduct)
                        .eq(ShopInfo::getId, depositShop.getId()));
                revenueMapper.insert(buildDeductRevenue(depositShop.getId(), Constants.TWO,
                        depositShopDeduct, order.getId(), order.getCode(), now));
            }
        }
        // å–件门店扣款
        if (takeShopDeduct != null && takeShopDeduct > 0 && order.getTakeShopId() != null) {
            ShopInfo takeShop = shopInfoMapper.selectById(order.getTakeShopId());
            if (takeShop != null) {
                shopInfoMapper.update(new UpdateWrapper<ShopInfo>().lambda()
                        .setSql(" BALANCE = IFNULL(BALANCE, 0) - " + takeShopDeduct)
                        .eq(ShopInfo::getId, takeShop.getId()));
                revenueMapper.insert(buildDeductRevenue(takeShop.getId(), Constants.TWO,
                        takeShopDeduct, order.getId(), order.getCode(), now));
            }
        }
        // å¸æœºæ‰£æ¬¾
        if (driverDeduct != null && driverDeduct > 0 && order.getAcceptDriver() != null) {
            DriverInfo driver = driverInfoMapper.selectById(order.getAcceptDriver());
            if (driver != null && driver.getMemberId() != null) {
                driverInfoMapper.update(new UpdateWrapper<DriverInfo>().lambda()
                        .setSql(" BALANCE = IFNULL(BALANCE, 0) - " + driverDeduct)
                        .eq(DriverInfo::getId, driver.getId()));
                revenueMapper.insert(buildDeductRevenue(driver.getMemberId(), Constants.ONE,
                        driverDeduct, order.getId(), order.getCode(), now));
            }
        }
    }
    private Revenue buildDeductRevenue(Integer memberId, Integer memberType, Long amount, Integer orderId, String orderNo, Date now) {
        Revenue revenue = new Revenue();
        revenue.setMemberId(memberId);
        revenue.setMemberType(memberType);
        revenue.setType(Constants.FOUR); // è´£ä»»æ‰£æ¬¾
        revenue.setOptType(-Constants.ONE); // æ”¯å‡º
        revenue.setAmount(amount);
        revenue.setVaildStatus(Constants.ONE); // å·²å…¥è´¦
        revenue.setObjId(orderId);
        revenue.setObjType(Constants.ZERO); // è®¢å•业务
        revenue.setOrderNo(orderNo);
        revenue.setStatus(Constants.ZERO); // æˆåŠŸ
        revenue.setDeleted(Constants.ZERO);
        revenue.setCreateTime(now);
        return revenue;
    }
    @Override
    public void processManualRefundCallback(OrdersRefund refundRecord) {
        if (!Constants.equalsInteger(refundRecord.getType(), Constants.FOUR)) {
            return;
        }
        if (StringUtils.isBlank(refundRecord.getDeductInfo())) {
            return;
        }
        Orders order = ordersMapper.selectById(refundRecord.getOrderId());
        if (order == null) {
            return;
        }
        JSONObject deductJson = JSONObject.parseObject(refundRecord.getDeductInfo());
        Long depositShopDeduct = deductJson.getLong("depositShopDeduct");
        Long takeShopDeduct = deductJson.getLong("takeShopDeduct");
        Long driverDeduct = deductJson.getLong("driverDeduct");
        processManualRefundDeduction(order, depositShopDeduct, takeShopDeduct, driverDeduct);
        // æ ‡è®°è®¢å•已手动退款,累加退款金额
        ordersMapper.update(new UpdateWrapper<Orders>().lambda()
                .set(Orders::getManualRefund, Constants.ONE)
                .setSql(" REFUND_AMOUNT = IFNULL(REFUND_AMOUNT, 0) + " + refundRecord.getRefundAmount())
                .set(Orders::getUpdateTime, new Date())
                .eq(Orders::getId, order.getId()));
    }
    /**
@@ -2258,6 +2587,10 @@
     * å‘送司机站内信通知
     */
    private void sendDriverNotice(Integer driverId, Constants.DriverOrderNotify notify, Integer orderId, String... params) {
        sendDriverNotice(driverId, notify, orderId, 0, params);
    }
    private void sendDriverNotice(Integer driverId, Constants.DriverOrderNotify notify, Integer objId, Integer objType, String... params) {
        DriverInfo driver = driverInfoMapper.selectById(driverId);
        if (driver == null || driver.getMemberId() == null) {
            return;
@@ -2267,8 +2600,8 @@
        notice.setUserId(driver.getMemberId());
        notice.setTitle(notify.getTitle());
        notice.setContent(notify.format(params));
        notice.setObjId(orderId);
        notice.setObjType(0); // 0=订单
        notice.setObjId(objId);
        notice.setObjType(objType);
        notice.setStatus(0);  // 0=未读
        notice.setIsdeleted(Constants.ZERO);
        notice.setCreateDate(new Date());
@@ -2910,9 +3243,9 @@
            order.setMemberVerifyCode(generateVerifyCode());
            ordersMapper.updateById(order);
            // å¯„存时图片必填
            if (images == null || images.isEmpty()) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "请上传寄存图片");
            }
//            if (images == null || images.isEmpty()) {
//                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "请上传寄存图片");
//            }
            // ä¿å­˜å¯„存图片(obj_type=2 è®¢å•寄存图片,最多3张)
            saveVerifyImages(order.getId(), images, Constants.FileType.ORDER_DEPOSIT.getKey(), shopId);
            // è®°å½•订单日志
@@ -2948,7 +3281,8 @@
            }
            // å¾…取件(5) â†’ å·²å®Œæˆ(7)
            order.setStatus(Constants.OrderStatus.finished.getStatus());
            order.setConfirmArriveTime(now);
            order.setFinishTime(now);
            order.setInvoiceStatus(Constants.ONE);
            ordersMapper.updateById(order);
            // å°±åœ°å¯„å­˜(type=0)取件时图片不必填,其他类型取件必填
            if (!Constants.equalsInteger(order.getType(), Constants.ZERO)) {
@@ -3096,6 +3430,30 @@
                sendOrderNotice(order.getMemberId(), Constants.MemberOrderNotify.WAIT_PICKUP_REMIND, order.getId(),
                        "orderNo", order.getCode(), "shopName", shopName);
            }
            // å¼‚常订单寄存核销:标记原订单完成
            if (Constants.equalsInteger(order.getExceptionStatus(), Constants.ONE) && order.getRelationOrderId() != null) {
                Orders originalOrder = ordersMapper.selectById(order.getRelationOrderId());
                if (originalOrder != null) {
                    originalOrder.setStatus(Constants.OrderStatus.finished.getStatus());
                    originalOrder.setInvoiceStatus(Constants.ONE);
                    originalOrder.setFinishTime(now);
                    originalOrder.setUpdateTime(now);
                    ordersMapper.updateById(originalOrder);
                    // è§¦å‘原订单收益计算
                    calculateAndSaveOrderFees(originalOrder.getId());
                    generateRevenueRecords(originalOrder.getId());
                    // é€šçŸ¥ä¼šå‘˜ï¼šè®¢å•已完成
                    sendOrderNotice(originalOrder.getMemberId(), Constants.MemberOrderNotify.FINISHED, originalOrder.getId(),
                            "orderNo", originalOrder.getCode());
                    // é€šçŸ¥å¸æœºï¼šè®¢å•已完成
                    if (originalOrder.getAcceptDriver() != null) {
                        String settleDays = operationConfigBiz.getConfig().getSettlementDate();
                        sendDriverNotice(originalOrder.getAcceptDriver(), Constants.DriverOrderNotify.FINISHED, originalOrder.getId(),
                                "orderNo", originalOrder.getCode(),
                                "settleDays", settleDays != null ? settleDays : "7");
                    }
                }
            }
        } else if (Constants.equalsInteger(status, Constants.OrderStatus.arrived.getStatus())) {
            // å¼‚地寄存 + æ— å–件门店 â†’ æ— æ³•核销(客户自取,无门店操作)
            if (Constants.equalsInteger(order.getType(), Constants.ONE) && order.getTakeShopId() == null) {
@@ -3115,6 +3473,7 @@
            }
            // å¾…取件(5) â†’ å·²å®Œæˆ(7)
            order.setStatus(Constants.OrderStatus.finished.getStatus());
            order.setInvoiceStatus(Constants.ONE);
            order.setConfirmArriveTime(now);
            ordersMapper.updateById(order);
            // è®¢å•完成,释放核销码
@@ -3191,6 +3550,7 @@
        // 7. æ›´æ–°è®¢å•状态为已完成
        Date now = new Date();
        order.setStatus(Constants.OrderStatus.finished.getStatus());
        order.setInvoiceStatus(Constants.ONE);
        order.setFinishTime(now);
        order.setUpdateTime(now);
        ordersMapper.updateById(order);
@@ -3280,10 +3640,15 @@
        if (!Constants.equalsInteger(order.getStatus(), Constants.OrderStatus.arrived.getStatus())) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前订单状态不允许确认收货");
        }
        // 5.1 æ ¡éªŒå¼‚常状态
        if (Constants.equalsInteger(order.getExceptionStatus(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "订单异常,已被转存,请联系管理员或查看短信");
        }
        // 6. æ›´æ–°è®¢å•状态为已完成
        Date now = new Date();
        order.setStatus(Constants.OrderStatus.finished.getStatus());
        order.setInvoiceStatus(Constants.ONE);
        order.setFinishTime(now);
        order.setUpdateTime(now);
        ordersMapper.updateById(order);
@@ -3320,7 +3685,6 @@
        BigDecimal depositRate = order.getDepositShopFeeRata() != null ? order.getDepositShopFeeRata() : BigDecimal.ZERO;
        BigDecimal takeRate = order.getTakeShopFeeRata() != null ? order.getTakeShopFeeRata() : BigDecimal.ZERO;
        BigDecimal driverRate = order.getDriverFeeRata() != null ? order.getDriverFeeRata() : BigDecimal.ZERO;
        Long exceptionFeeVal = order.getExceptionFee() != null ? order.getExceptionFee() : 0L;
        //存件门店收益
        Long depositShopFee = new BigDecimal(totalAmount)
@@ -3336,8 +3700,7 @@
            driverFee = new BigDecimal(totalAmount)
                    .multiply(driverRate)
                    .setScale(0, RoundingMode.HALF_UP)
                    .longValue()
                    + exceptionFeeVal;
                    .longValue();
            // å¼‚地寄存且有取件门店:加上取件门店收益
            if (order.getTakeShopId() != null) {
@@ -3396,6 +3759,91 @@
                        driverFee, orderId, order.getCode()));
            }
        }
        // å¼‚常订单:司机异常补偿(异地 + å¼‚常标记 + æœ‰å¼‚常补偿金额)
        if (Constants.equalsInteger(order.getType(), Constants.ONE)
                && Constants.equalsInteger(order.getExceptionStatus(), Constants.ONE)
                && order.getExceptionFee() != null && order.getExceptionFee() > 0
                && order.getAcceptDriver() != null) {
            DriverInfo driver = driverInfoMapper.selectById(order.getAcceptDriver());
            if (driver != null && driver.getMemberId() != null) {
                Revenue exRevenue = new Revenue();
                exRevenue.setMemberId(driver.getMemberId());
                exRevenue.setMemberType(Constants.ONE); // 1=司机
                exRevenue.setType(5); // 5=异常金额
                exRevenue.setOptType(Constants.ONE); // 1=收入
                exRevenue.setAmount(order.getExceptionFee());
                exRevenue.setVaildStatus(Constants.ZERO);
                exRevenue.setObjId(orderId);
                exRevenue.setObjType(Constants.ZERO);
                exRevenue.setStatus(Constants.ZERO);
                exRevenue.setOrderNo(order.getCode());
                exRevenue.setDeleted(Constants.ZERO);
                exRevenue.setCreateTime(now);
                revenueMapper.insert(exRevenue);
            }
        }
        // å¸æœºå¥–励金(仅生成奖励记录,不直接入账)
        generateDriverReward(order);
    }
    /**
     * ç”Ÿæˆå¸æœºå¥–励记录
     * è§„则:
     *   å·²å¥–励次数 < registerRewardOrderCount â†’ æ³¨å†Œå¥–励(type=0, amount=registerRewardAmount)
     *   registerRewardOrderCount <= å·²å¥–励次数 < (registerRewardOrderCount + platformRewardOrderCount) â†’ å¹³å°å¥–励(type=1, amount=platformRewardAmount)
     *   è¶…出总名额不奖励
     */
    private void generateDriverReward(Orders order) {
        if (order.getAcceptDriver() == null) return;
        DriverInfo driver = driverInfoMapper.selectById(order.getAcceptDriver());
        if (driver == null || driver.getId() == null) return;
        // è¯»å–奖励配置
        String registerCountStr = operationConfigBiz.getConfig().getRegisterRewardOrderCount();
        String registerAmountStr = operationConfigBiz.getConfig().getRegisterRewardAmount();
        String platformCountStr = operationConfigBiz.getConfig().getPlatformRewardOrderCount();
        String platformAmountStr = operationConfigBiz.getConfig().getPlatformRewardAmount();
        if (StringUtils.isAnyBlank(registerCountStr, registerAmountStr, platformCountStr, platformAmountStr)) return;
        int registerCount = Integer.parseInt(registerCountStr);
        long registerAmount = new BigDecimal(registerAmountStr).multiply(BigDecimal.valueOf(100)).longValue();
        int platformCount = Integer.parseInt(platformCountStr);
        long platformAmount = new BigDecimal(platformAmountStr).multiply(BigDecimal.valueOf(100)).longValue();
        int totalRewardSlots = registerCount + platformCount; // æ€»å¥–励名额
        // æŸ¥è¯¢å¸æœºå·²å¥–励次数
        Long rewarded = rewardRecordMapper.selectCount(new QueryWrapper<RewardRecord>().lambda()
                .eq(RewardRecord::getDriverId, driver.getId())
                .eq(RewardRecord::getDeleted, Constants.ZERO));
        int count = rewarded.intValue();
        if (count >= totalRewardSlots) return; // å·²è¶…出奖励名额
        // åˆ¤æ–­æœ¬æ¬¡å¥–励类型和金额
        int rewardType;
        long rewardAmount;
        if (count < registerCount) {
            // æ³¨å†Œå¥–励阶段
            rewardType = Constants.ZERO;
            rewardAmount = registerAmount;
        } else {
            // å¹³å°å¥–励阶段
            rewardType = Constants.ONE;
            rewardAmount = platformAmount;
        }
        if (rewardAmount <= 0) return;
        // åˆ›å»ºå¥–励记录(状态=0待领取,后续由司机领取时入账)
        RewardRecord record = new RewardRecord();
        record.setOrderId(order.getId());
        record.setDriverId(driver.getId());
        record.setStatus(Constants.ZERO); // 0=待领取
        record.setType(rewardType);       // 0=注册奖励, 1=平台奖励
        record.setAmount(rewardAmount);
        record.setDeleted(Constants.ZERO);
        record.setCreateTime(new Date());
        rewardRecordMapper.insert(record);
    }
    /**
@@ -3558,9 +4006,57 @@
                        .eq(OrdersDetail::getOrderId, orderId)
                        .eq(OrdersDetail::getDeleted, Constants.ZERO));
        OverdueFeeVO overdueInfo = calculateOverdueFeeInternal(order, details);
        Date now = new Date();
        // å¼‚常订单逾期逻辑
        if (Constants.equalsInteger(order.getExceptionStatus(), Constants.ONE)
                && order.getRelationOrderId() != null) {
            Orders originalOrder = ordersMapper.selectById(order.getRelationOrderId());
            long driverExceptionFee = (originalOrder != null && originalOrder.getExceptionFee() != null)
                    ? originalOrder.getExceptionFee() : 0L;
            long shopExceptionFee = order.getShopCompensationAmount() != null ? order.getShopCompensationAmount() : 0L;
            long totalExceptionFee = driverExceptionFee + shopExceptionFee;
            // åˆ¤æ–­æ˜¯å¦å½“天取件(按 expectedTakeTime æ—¥æœŸï¼‰
            boolean sameDay = isSameDay(now, order.getExpectedTakeTime());
            long overdueFee;
            int overdueDays;
            if (sameDay) {
                // å½“天取件:逾期费用 = å¼‚常费用总和
                overdueFee = totalExceptionFee;
                overdueDays = totalExceptionFee > 0 ? 1 : 0;
            } else {
                // éžå½“天取件:正常逾期计算 + å¸æœºå¼‚常费用(不含门店异常费用)
                OverdueFeeVO normalOverdue = calculateOverdueFeeInternal(order, details);
                long normalFee = (normalOverdue != null && normalOverdue.getOverdueFee() != null)
                        ? normalOverdue.getOverdueFee() : 0L;
                overdueFee = normalFee + driverExceptionFee;
                overdueDays = (normalOverdue != null && normalOverdue.getOverdueDays() != null)
                        ? normalOverdue.getOverdueDays() : 0;
            }
            order.setConfirmArriveTime(now);
            order.setUpdateTime(now);
            if (overdueFee > 0) {
                order.setOverdueStatus(Constants.ONE);
                order.setOverdueDays(overdueDays);
                order.setOverdueAmount(overdueFee);
                ordersMapper.updateById(order);
                String overdueLogInfo = "门店【" + shopName + "】确认顾客到店,逾期" + overdueDays
                        + "天,逾期费用" + Constants.getFormatMoney(overdueFee) + "元";
                saveShopVerifyLog(order, Constants.OrderLogType.shopConfirmArriveOverdue, overdueLogInfo, null, shopId);
            } else {
                order.setOverdueStatus(Constants.ZERO);
                ordersMapper.updateById(order);
                saveShopVerifyLog(order, Constants.OrderLogType.shopConfirmArrive,
                        "门店【" + shopName + "】确认顾客到店,未逾期", null, shopId);
            }
            return;
        }
        // æ™®é€šè®¢å•逾期计算
        OverdueFeeVO overdueInfo = calculateOverdueFeeInternal(order, details);
        if (overdueInfo.getOverdue() && overdueInfo.getOverdueDays() > 0) {
            // å­˜åœ¨é€¾æœŸï¼šæ ‡è®°é€¾æœŸçŠ¶æ€ï¼Œè®¢å•ä¿æŒå½“å‰çŠ¶æ€
@@ -3890,6 +4386,16 @@
        long diffMs = nowCal.getTimeInMillis() - depositCal.getTimeInMillis();
        int days = (int) (diffMs / (1000 * 60 * 60 * 24));
        return Math.max(days, 1);
    }
    private boolean isSameDay(Date d1, Date d2) {
        if (d1 == null || d2 == null) return false;
        Calendar c1 = Calendar.getInstance();
        c1.setTime(d1);
        Calendar c2 = Calendar.getInstance();
        c2.setTime(d2);
        return c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR)
                && c1.get(Calendar.DAY_OF_YEAR) == c2.get(Calendar.DAY_OF_YEAR);
    }
    /**
@@ -4229,6 +4735,7 @@
                // æ›´æ–°è®¢å•状态为已完成
                order.setStatus(Constants.OrderStatus.finished.getStatus());
                order.setInvoiceStatus(Constants.ONE);
                order.setFinishTime(now);
                order.setUpdateTime(now);
                ordersMapper.updateById(order);
@@ -4450,6 +4957,9 @@
        Date now = new Date();
        Date threshold = new Date(now.getTime() + (long) minutes * 60 * 1000);
        // æŸ¥è¯¢ï¼šstatus=5、未通知、就地寄存 or å¼‚地(有取件门店)、预计取件时间在 now~threshold ä¹‹é—´
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String nowStr = sdf.format(now);
        String thresholdStr = sdf.format(threshold);
        List<Orders> orders = ordersMapper.selectList(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getStatus, Constants.OrderStatus.arrived.getStatus())
                .eq(Orders::getPayStatus, Constants.ONE)
@@ -4460,8 +4970,8 @@
                )
                .ne(Orders::getPickUpNotifyStatus, Constants.ONE)
                .isNotNull(Orders::getExpectedTakeTime)
                .ge(Orders::getExpectedTakeTime, now)
                .le(Orders::getExpectedTakeTime, threshold));
                .apply("DATE_FORMAT(EXPECTED_TAKE_TIME, '%Y-%m-%d %H:%i:%s') >= {0}", nowStr)
                .apply("DATE_FORMAT(EXPECTED_TAKE_TIME, '%Y-%m-%d %H:%i:%s') <= {0}", thresholdStr));
        if (orders == null || orders.isEmpty()) {
            return 0;
        }
@@ -4558,4 +5068,235 @@
        return distanceKm * 1000 <= radiusM;
    }
    @Override
    @Transactional(rollbackFor = {Exception.class, BusinessException.class})
    public void handleOrderException(HandleOrderExceptionDTO dto) {
        // ========== A. æ ¡éªŒ ==========
        Orders original = ordersMapper.selectById(dto.getOrderId());
        if (original == null || Constants.equalsInteger(original.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "订单不存在");
        }
        if (!Constants.equalsInteger(original.getType(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅支持异地订单异常处理");
        }
        if (original.getTakeShopId() != null) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单已关联取件门店,不支持异常处理");
        }
        if (!Constants.equalsInteger(original.getStatus(), Constants.FIVE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "仅支持已送达状态的订单");
        }
        if (Constants.equalsInteger(original.getExceptionStatus(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该订单已处理过异常,请勿重复操作");
        }
        ShopInfo newShop = shopInfoMapper.selectById(dto.getDepositShopId());
        if (newShop == null || Constants.equalsInteger(newShop.getStatus(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "存放门店不存在或已禁用");
        }
        Date now = new Date();
        // ========== B. åˆ›å»ºæ–°è®¢å•(就地存取) ==========
        String orderCode = "JC" + new java.text.SimpleDateFormat("yyyyMMddHHmmss").format(now)
                + String.format("%04d", new java.util.Random().nextInt(10000));
        String orderTradeNo = generateOrderTradeNo();
        // å½“天23:30
        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.HOUR_OF_DAY, 23);
        cal.set(Calendar.MINUTE, 30);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        Date expectedTakeTime = cal.getTime();
        // è®¡ç®—基础单价
        Long price = 0L;
        Long basicAmount = 0L;
        // æŸ¥è¯¢åŽŸè®¢å•æ˜Žç»†èŽ·å–ç‰©å“åˆ—è¡¨
        List<OrdersDetail> originalDetails = ordersDetailMapper.selectList(
                new QueryWrapper<OrdersDetail>().lambda()
                        .eq(OrdersDetail::getOrderId, original.getId())
                        .eq(OrdersDetail::getDeleted, Constants.ZERO));
        if (!CollectionUtils.isEmpty(originalDetails)) {
            CalculateLocalPriceDTO priceDTO = new CalculateLocalPriceDTO();
            priceDTO.setCityId(Integer.valueOf(original.getCityId()));
            priceDTO.setDepositStartTime(now);
            priceDTO.setDepositEndTime(expectedTakeTime);
            List<OrderItemDTO> items = new ArrayList<>();
            for (OrdersDetail d : originalDetails) {
                OrderItemDTO item = new OrderItemDTO();
                item.setCategoryId(d.getLuggageId());
                item.setQuantity(d.getNum());
                items.add(item);
            }
            priceDTO.setItems(items);
            PriceCalculateVO priceResult = calculateLocalPrice(priceDTO);
            if (priceResult != null) {
                price = priceResult.getItemPrice();
                basicAmount = priceResult.getItemPrice();
            }
        }
        // å­˜ä»¶é—¨åº—分成占比(就地存取)
        Integer cityId = Integer.valueOf(original.getCityId());
        boolean isCompany = Constants.equalsInteger(newShop.getCompanyType(), Constants.ONE);
        int fallbackFieldA = isCompany ? Constants.FIVE : Constants.SIX;
        BigDecimal depositShopFeeRate = getShopRevenueShare(newShop, "localDeposit", cityId, fallbackFieldA);
        Orders newOrder = new Orders();
        newOrder.setCode(orderCode);
        newOrder.setOutTradeNo(orderTradeNo);
        newOrder.setMemberId(original.getMemberId());
        newOrder.setType(Constants.ZERO); // å°±åœ°å­˜å–
        newOrder.setCityId(original.getCityId());
        newOrder.setStatus(Constants.ONE); // å¾…寄存
        newOrder.setPayStatus(Constants.ONE); // å·²æ”¯ä»˜
        newOrder.setPayTime(now);
        newOrder.setCommentStatus(Constants.ZERO);
        newOrder.setSettlementStatus(Constants.ZERO);
        newOrder.setDeleted(Constants.ZERO);
        newOrder.setCreateTime(now);
        newOrder.setUpdateTime(now);
        newOrder.setIsConverted(Constants.ZERO);
        // å­˜ä»¶ä¿¡æ¯ï¼ˆå…¥å‚门店)
        newOrder.setDepositShopId(dto.getDepositShopId());
        newOrder.setDepositLocation(newShop.getAddress());
        newOrder.setDepositLocationRemark(newShop.getAddress());
        newOrder.setDepositLat(BigDecimal.valueOf(newShop.getLatitude()));
        newOrder.setDepositLgt(BigDecimal.valueOf(newShop.getLongitude()));
        // å–件信息(就地=存件门店)
        newOrder.setTakeShopId(dto.getDepositShopId());
        newOrder.setTakeLocation(newShop.getAddress());
        newOrder.setTakeLocationRemark(newShop.getAddress());
        newOrder.setTakeLat(BigDecimal.valueOf(newShop.getLatitude()));
        newOrder.setTakeLgt(BigDecimal.valueOf(newShop.getLongitude()));
        // æ—¶é—´
        newOrder.setExpectedDepositTime(now);
        newOrder.setExpectedTakeTime(expectedTakeTime);
        newOrder.setEstimatedDepositDays(1);
        // ç‰©å“ä¿¡æ¯
        newOrder.setGoodType(original.getGoodType());
        newOrder.setGoodLevel(original.getGoodLevel());
        newOrder.setGoodsInfo(original.getGoodsInfo());
        newOrder.setSupplement(original.getSupplement());
        newOrder.setSelfTake(original.getSelfTake());
        newOrder.setTakeUser(original.getTakeUser());
        newOrder.setTakePhone(original.getTakePhone());
        // å¼‚常标记
        newOrder.setExceptionStatus(Constants.ONE);
        newOrder.setRelationOrderId(original.getId());
        newOrder.setExceptionInfo(dto.getRemark());
        // è´¹ç”¨
        newOrder.setIsUrgent(Constants.ZERO);
        newOrder.setUrgentRata(BigDecimal.ONE);
        newOrder.setUrgentAmount(0L);
        newOrder.setDeclaredAmount(0L);
        newOrder.setDeclaredFee(0L);
        newOrder.setPrice(price);
        newOrder.setBasicAmount(basicAmount);
        newOrder.setEstimatedAmount(basicAmount);
        newOrder.setTotalAmount(0L);
        newOrder.setPayAmount(0L);
        newOrder.setManualRefund(Constants.ZERO);
        // è´¹ç”¨åˆ†é…
        newOrder.setDriverFee(0L);
        newOrder.setDriverFeeRata(BigDecimal.ZERO);
        newOrder.setDepositShopFee(0L);
        newOrder.setDepositShopFeeRata(depositShopFeeRate);
        newOrder.setTakeShopFee(0L);
        newOrder.setTakeShopFeeRata(BigDecimal.ZERO);
        // é—¨åº—补偿金额
        newOrder.setShopCompensationAmount(dto.getShopCompensation());
        newOrder.setRemark(dto.getRemark());
        newOrder.setPlatformSmsNotified(Constants.ZERO);
        newOrder.setMemberVerifyCode(generateVerifyCode());
        newOrder.setDriverVerifyCode(generateVerifyCode());
        // è®¡ç®—店铺订单序号
        Date todayStart = DateUtil.getStartOfDay(now);
        Long currentCount = ordersMapper.selectCount(new QueryWrapper<Orders>().lambda()
                .eq(Orders::getDepositShopId, dto.getDepositShopId())
                .eq(Orders::getPayStatus, Constants.ONE)
                .ge(Orders::getPayTime, todayStart)
                .eq(Orders::getDeleted, Constants.ZERO));
        newOrder.setAutoNum(currentCount + 1);
        ordersMapper.insert(newOrder);
        // ========== C. åˆ›å»ºæ–°è®¢å•明细 ==========
        if (!CollectionUtils.isEmpty(originalDetails)) {
            for (OrdersDetail od : originalDetails) {
                OrdersDetail detail = new OrdersDetail();
                detail.setOrderId(newOrder.getId());
                detail.setLuggageId(od.getLuggageId());
                detail.setLuggageName(od.getLuggageName());
                detail.setLuggageDetail(od.getLuggageDetail());
                detail.setNum(od.getNum());
                detail.setUnitPrice(od.getLocallyPrice());
                detail.setLocallyPrice(od.getLocallyPrice());
                detail.setDeleted(Constants.ZERO);
                detail.setCreateTime(now);
                ordersDetailMapper.insert(detail);
            }
        }
        // ========== D. æ‹·è´é™„ä»¶ ==========
        List<Multifile> originalFiles = multifileMapper.selectList(
                new QueryWrapper<Multifile>().lambda()
                        .eq(Multifile::getObjId, original.getId())
                        .eq(Multifile::getObjType, Constants.FileType.ORDER_FILE.getKey())
                        .eq(Multifile::getIsdeleted, Constants.ZERO));
        if (!CollectionUtils.isEmpty(originalFiles)) {
            int sortNum = 1;
            for (Multifile mf : originalFiles) {
                Multifile newFile = new Multifile();
                newFile.setObjId(newOrder.getId());
                newFile.setObjType(Constants.FileType.ORDER_FILE.getKey());
                newFile.setType(mf.getType());
                newFile.setFileurl(mf.getFileurl());
                newFile.setIsdeleted(Constants.ZERO);
                newFile.setCreateDate(now);
                newFile.setSortnum(sortNum++);
                multifileMapper.insert(newFile);
            }
        }
        // ========== E. æ›´æ–°åŽŸè®¢å• ==========
        ordersMapper.update(new UpdateWrapper<Orders>().lambda()
                .set(Orders::getExceptionStatus, Constants.ONE)
                .set(Orders::getExceptionFee, dto.getDriverCompensation())
                .set(Orders::getUpdateTime, now)
                .eq(Orders::getId, original.getId()));
        // ========== F. å‘送短信通知 ==========
        // å¸æœºç«¯-异常派单
        if (original.getAcceptDriver() != null) {
            DriverInfo driver = driverInfoMapper.selectById(original.getAcceptDriver());
            if (driver != null && StringUtils.isNotBlank(driver.getTelephone())) {
                sendSmsNotify(driver.getTelephone(), Constants.SmsNotify.EXCEPTION_DISPATCH,
                        "orderNo", original.getCode(),
                        "address", newShop.getAddress(),
                        "code", newOrder.getMemberVerifyCode());
            }
        }
        // ä¼šå‘˜ç«¯-异常派单
        String memberPhone = StringUtils.isNotBlank(original.getTakePhone()) ? original.getTakePhone() : null;
        if (memberPhone == null) {
            Member member = memberMapper.selectById(original.getMemberId());
            if (member != null) {
                memberPhone = member.getTelephone();
            }
        }
        if (StringUtils.isNotBlank(memberPhone)) {
            sendSmsNotify(memberPhone, Constants.SmsNotify.MEMBER_EXCEPTION_DISPATCH,
                    "address", newShop.getAddress());
        }
    }
}
server/services/src/main/java/com/doumee/service/business/impl/RevenueServiceImpl.java
@@ -20,10 +20,15 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.sql.Driver;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.doumee.dao.dto.ShopRevenueQueryDTO;
import com.doumee.dao.vo.DriverKpiVO;
import com.doumee.dao.vo.DriverOrderTrendVO;
import com.doumee.dao.vo.DriverRewardHallVO;
import com.doumee.biz.system.OperationConfigBiz;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
/**
 * æ”¶æ”¯è®°å½•Service实现
@@ -47,6 +52,15 @@
    @Autowired
    private DriverInfoMapper driverInfoMapper;
    @Autowired
    private OrdersMapper ordersMapper;
    @Autowired
    private RewardRecordMapper rewardRecordMapper;
    @Autowired
    private OperationConfigBiz operationConfigBiz;
    @Override
    public Integer create(Revenue revenue) {
@@ -348,4 +362,218 @@
        return vo;
    }
    @Override
    public DriverKpiVO getDriverKpi(Integer driverId, ShopRevenueQueryDTO query) {
        QueryWrapper<Orders> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .eq(Orders::getType, Constants.ONE)
                .eq(Orders::getAcceptDriver, driverId)
                .in(Orders::getStatus,
                        Constants.OrderStatus.waitDeposit.getKey(),
                        Constants.OrderStatus.deposited.getKey(),
                        Constants.OrderStatus.accepted.getKey(),
                        Constants.OrderStatus.delivering.getKey(),
                        Constants.OrderStatus.arrived.getKey(),
                        Constants.OrderStatus.finished.getKey());
        if (query.getStartDate() != null) {
            qw.lambda().ge(Orders::getCreateTime, query.getStartDate());
        }
        if (query.getEndDate() != null) {
            qw.lambda().le(Orders::getCreateTime, Utils.Date.getEnd(query.getEndDate()));
        }
        List<Orders> orders = ordersMapper.selectList(qw);
        DriverKpiVO vo = new DriverKpiVO();
        vo.setTotalOrderCount(orders.size());
        // æ€»å®Œæˆè®¢å•量
        long finishedCount = orders.stream()
                .filter(o -> Constants.equalsInteger(o.getStatus(), Constants.OrderStatus.finished.getStatus()))
                .count();
        vo.setFinishedOrderCount((int) finishedCount);
        // æ€»è¥æ”¶é‡‘额
        long totalRevenue = orders.stream()
                .mapToLong(o -> {
                    long total = o.getTotalAmount() != null ? o.getTotalAmount() : 0L;
                    long refund = o.getRefundAmount() != null ? o.getRefundAmount() : 0L;
                    return total - refund;
                }).sum();
        vo.setTotalRevenue(totalRevenue);
        // å¸æœºåˆ†æˆé‡‘额
        long driverFee = orders.stream()
                .mapToLong(o -> o.getDriverFee() != null ? o.getDriverFee() : 0L)
                .sum();
        vo.setDriverFeeTotal(driverFee);
        // é€€æ¬¾å•æ•°
        long refundCount = orders.stream()
                .filter(o -> o.getRefundAmount() != null && o.getRefundAmount() > 0)
                .count();
        vo.setRefundOrderCount((int) refundCount);
        // è´£ä»»æ‰£æ¬¾æ€»é¢ï¼šRevenue memberId=driverId, memberType=1, type=4
        QueryWrapper<Revenue> revQw = new QueryWrapper<>();
        revQw.lambda()
                .eq(Revenue::getMemberId, driverId)
                .eq(Revenue::getMemberType, Constants.ONE)
                .eq(Revenue::getType, Constants.FOUR)
                .eq(Revenue::getDeleted, Constants.ZERO);
        if (query.getStartDate() != null) {
            revQw.lambda().ge(Revenue::getCreateTime, query.getStartDate());
        }
        if (query.getEndDate() != null) {
            revQw.lambda().le(Revenue::getCreateTime, Utils.Date.getEnd(query.getEndDate()));
        }
        List<Revenue> deductRecords = revenueMapper.selectList(revQw);
        long deductTotal = deductRecords.stream()
                .mapToLong(r -> r.getAmount() != null ? r.getAmount() : 0L)
                .sum();
        vo.setDeductTotal(deductTotal);
        return vo;
    }
    @Override
    public List<DriverOrderTrendVO> getDriverOrderTrend(Integer driverId) {
        SimpleDateFormat sdf = new SimpleDateFormat("MM/dd");
        // æž„建近7天日期列表
        List<String> dateList = new ArrayList<>();
        for (int i = 6; i >= 0; i--) {
            Calendar c = Calendar.getInstance();
            c.add(Calendar.DAY_OF_MONTH, -i);
            dateList.add(sdf.format(c.getTime()));
        }
        // æŸ¥è¯¢è¿‘7天订单
        Calendar startCal = Calendar.getInstance();
        startCal.add(Calendar.DAY_OF_MONTH, -6);
        startCal.set(Calendar.HOUR_OF_DAY, 0);
        startCal.set(Calendar.MINUTE, 0);
        startCal.set(Calendar.SECOND, 0);
        startCal.set(Calendar.MILLISECOND, 0);
        QueryWrapper<Orders> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .eq(Orders::getAcceptDriver, driverId)
                .in(Orders::getStatus,
                        Constants.OrderStatus.accepted.getKey(),
                        Constants.OrderStatus.delivering.getKey(),
                        Constants.OrderStatus.arrived.getKey(),
                        Constants.OrderStatus.finished.getKey())
                .ge(Orders::getCreateTime, startCal.getTime());
        List<Orders> orders = ordersMapper.selectList(qw);
        // æŒ‰æ—¥æœŸåˆ†ç»„统计
        Map<String, Long> countMap = orders.stream()
                .collect(Collectors.groupingBy(o -> sdf.format(o.getCreateTime()), Collectors.counting()));
        List<DriverOrderTrendVO> result = new ArrayList<>();
        for (String date : dateList) {
            DriverOrderTrendVO item = new DriverOrderTrendVO();
            item.setDate(date);
            item.setOrderCount(countMap.getOrDefault(date, 0L).intValue());
            result.add(item);
        }
        return result;
    }
    @Override
    public DriverRewardHallVO getDriverRewardHall(Integer driverId) {
        // æŸ¥è¯¢å·²é¢†å–金额(status=1)
        QueryWrapper<RewardRecord> claimedQw = new QueryWrapper<>();
        claimedQw.select("IFNULL(SUM(AMOUNT), 0) as amount")
                .eq("DRIVER_ID", driverId)
                .eq("STATUS", Constants.ONE)
                .eq("DELETED", Constants.ZERO);
        Map<String, Object> claimedResult = rewardRecordMapper.selectMaps(claimedQw).stream().findFirst().orElse(null);
        long claimedAmount = claimedResult != null && claimedResult.get("amount") != null
                ? Long.parseLong(claimedResult.get("amount").toString()) : 0L;
        // æŸ¥è¯¢å¾…领取金额(status=0)
        QueryWrapper<RewardRecord> pendingQw = new QueryWrapper<>();
        pendingQw.select("IFNULL(SUM(AMOUNT), 0) as amount")
                .eq("DRIVER_ID", driverId)
                .eq("STATUS", Constants.ZERO)
                .eq("DELETED", Constants.ZERO);
        Map<String, Object> pendingResult = rewardRecordMapper.selectMaps(pendingQw).stream().findFirst().orElse(null);
        long pendingAmount = pendingResult != null && pendingResult.get("amount") != null
                ? Long.parseLong(pendingResult.get("amount").toString()) : 0L;
        // è¯»å–奖励规则配置
        com.doumee.dao.dto.OperationConfigDTO config = operationConfigBiz.getConfig();
        // æŸ¥è¯¢å¥–励记录列表(按主键、创建时间升序)
        List<RewardRecord> records = rewardRecordMapper.selectList(new QueryWrapper<RewardRecord>().lambda()
                .eq(RewardRecord::getDriverId, driverId)
                .eq(RewardRecord::getDeleted, Constants.ZERO)
                .orderByAsc(RewardRecord::getId)
                .orderByAsc(RewardRecord::getCreateTime));
        DriverRewardHallVO vo = new DriverRewardHallVO();
        vo.setClaimedAmount(claimedAmount);
        vo.setPendingAmount(pendingAmount);
        vo.setRegisterRewardOrderCount(config.getRegisterRewardOrderCount());
        vo.setRegisterRewardAmount(config.getRegisterRewardAmount());
        vo.setPlatformRewardOrderCount(config.getPlatformRewardOrderCount());
        vo.setPlatformRewardAmount(config.getPlatformRewardAmount());
        vo.setRecords(records);
        return vo;
    }
    @Override
    public void claimReward(Integer driverId, Integer rewardRecordId) {
        // æŸ¥è¯¢å¥–励记录
        RewardRecord record = rewardRecordMapper.selectById(rewardRecordId);
        if (record == null || Constants.equalsInteger(record.getDeleted(), Constants.ONE)) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "奖励记录不存在");
        }
        if (!Constants.equalsInteger(record.getDriverId(), driverId)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "无权领取该奖励");
        }
        if (!Constants.equalsInteger(record.getStatus(), Constants.ZERO)) {
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "该奖励已领取");
        }
        Long rewardAmount = record.getAmount() != null ? record.getAmount() : 0L;
        if (rewardAmount <= 0) return;
        // æŸ¥è¯¢å¸æœºä¿¡æ¯
        DriverInfo driver = driverInfoMapper.selectById(driverId);
        if (driver == null) {
            throw new BusinessException(ResponseStatus.DATA_EMPTY.getCode(), "司机信息不存在");
        }
        // åˆ›å»ºæ”¶ç›Šè®°å½•(type=3 å¹³å°å¥–励, optType=1 æ”¶å…¥, vaildStatus=1 å·²å…¥è´¦ï¼‰
        Revenue revenue = new Revenue();
        revenue.setMemberId(driver.getMemberId());
        revenue.setMemberType(Constants.ONE);
        revenue.setType(Constants.THREE);      // å¹³å°å¥–励
        revenue.setOptType(Constants.ONE);     // æ”¶å…¥
        revenue.setAmount(rewardAmount);
        revenue.setVaildStatus(Constants.ONE); // å·²å…¥è´¦
        revenue.setObjId(record.getOrderId());
        revenue.setObjType(Constants.ZERO);
        revenue.setStatus(Constants.ZERO);
        revenue.setDeleted(Constants.ZERO);
        revenue.setCreateTime(new Date());
        revenueMapper.insert(revenue);
        // æ›´æ–°å¥–励记录:status=1已领取, claimTime, revenueId
        record.setStatus(Constants.ONE);
        record.setClaimTime(new Date());
        record.setRevenueId(revenue.getId());
        record.setUpdateTime(new Date());
        rewardRecordMapper.updateById(record);
        // æ›´æ–°å¸æœºä½™é¢
        driverInfoMapper.update(new UpdateWrapper<DriverInfo>().lambda()
                .setSql("BALANCE = IFNULL(BALANCE, 0) + " + rewardAmount)
                .setSql("TOTAL_BALANCE = IFNULL(TOTAL_BALANCE, 0) + " + rewardAmount)
                .eq(DriverInfo::getId, driverId));
    }
}
server/services/src/main/java/com/doumee/service/business/impl/ShopInfoServiceImpl.java
@@ -15,16 +15,20 @@
import com.doumee.biz.system.AreasBiz;
import com.doumee.biz.system.SystemDictDataBiz;
import com.doumee.core.utils.aliyun.AliSmsService;
import com.doumee.dao.business.CategoryMapper;
import com.doumee.dao.business.MemberMapper;
import com.doumee.dao.business.MultifileMapper;
import com.doumee.dao.business.OrdersMapper;
import com.doumee.dao.business.PricingRuleMapper;
import com.doumee.dao.business.RevenueMapper;
import com.doumee.dao.business.ShopInfoMapper;
import com.doumee.dao.business.model.Areas;
import com.doumee.dao.business.model.Category;
import com.doumee.dao.business.model.Member;
import com.doumee.dao.business.model.Multifile;
import com.doumee.dao.business.model.Orders;
import com.doumee.dao.business.model.PricingRule;
import com.doumee.dao.business.model.Revenue;
import com.doumee.dao.business.model.ShopInfo;
import com.doumee.config.xyy.XyyConfig;
import com.doumee.config.xyy.dto.AddPrinterRequest;
@@ -36,13 +40,17 @@
import com.doumee.dao.system.model.SystemDictData;
import com.doumee.dao.system.SystemUserMapper;
import com.doumee.dao.system.model.SystemUser;
import com.doumee.dao.vo.LuggageTypeItem;
import com.doumee.dao.vo.ShopDetailVO;
import com.doumee.dao.vo.ShopCenterVO;
import com.doumee.dao.vo.ShopKpiVO;
import com.doumee.dao.vo.ShopLoginVO;
import com.doumee.dao.vo.LocationTagShopCountVO;
import com.doumee.dao.vo.ShopNearbyVO;
import com.doumee.dao.vo.ShopSalesStatsVO;
import com.doumee.dao.vo.ShopWebDetailVO;
import com.doumee.service.business.AreasService;
import com.doumee.service.business.DataBoardService;
import com.doumee.service.business.ShopInfoService;
import com.doumee.dao.business.SmsrecordMapper;
import com.doumee.dao.business.model.Smsrecord;
@@ -105,6 +113,16 @@
    @Autowired
    private XyyConfig xyyConfig;
    @Autowired
    private DataBoardService dataBoardService;
    @Autowired
    private RevenueMapper revenueMapper;
    @Autowired
    private CategoryMapper categoryMapper;
    @Override
    public Integer create(ShopInfo shopInfo) {
        shopInfoMapper.insert(shopInfo);
@@ -329,6 +347,15 @@
            // ä¿å­˜æ–°å˜æ›´ç‰ˆæœ¬é™„ä»¶
            saveShopAttachments(newChange.getId(), request, now);
            // æ ‡è®°åŽ†å²çš„å˜æ›´ç‰ˆæœ¬ä¸ºåˆ é™¤
            shopInfoMapper.update(null, new UpdateWrapper<ShopInfo>().lambda()
                    .eq(ShopInfo::getRegionMemberId, member.getId())
                    .eq(ShopInfo::getVersionType, Constants.ONE)
                    .eq(ShopInfo::getDeleted, Constants.ZERO)
                    .ne(ShopInfo::getId, newChange.getId())
                    .set(ShopInfo::getDeleted, Constants.ONE)
                    .set(ShopInfo::getUpdateTime, now));
        } else {
            // æœ€æ–°å˜æ›´ç‰ˆæœ¬ status=0(待审批) æˆ– 2(被驳回):直接更新
            String rawPassword = generateDefaultPassword(request.getTelephone());
@@ -474,6 +501,19 @@
            throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "当前状态不允许审批");
        }
        // å®¡æ‰¹é€šè¿‡æ—¶ï¼Œæ”¶ç›Šæ¯”例配置必填
        String revenueShareConfig = null;
        if (Constants.equalsInteger(auditDTO.getAuditStatus(), Constants.ZERO)) {
            if (auditDTO.getLocalDeposit() == null || auditDTO.getRemoteDeposit() == null || auditDTO.getRemoteTake() == null) {
                throw new BusinessException(ResponseStatus.BAD_REQUEST.getCode(), "审批通过时收益比例配置不能为空");
            }
            JSONObject json = new JSONObject();
            json.put("localDeposit", auditDTO.getLocalDeposit());
            json.put("remoteDeposit", auditDTO.getRemoteDeposit());
            json.put("remoteTake", auditDTO.getRemoteTake());
            revenueShareConfig = json.toJSONString();
        }
        Date now = new Date();
        // auditDTO.auditStatus: 0=通过 â†’ 1, 1=驳回 â†’ 2
        Integer newAuditStatus = Constants.equalsInteger(auditDTO.getAuditStatus(), Constants.ZERO) ? Constants.ONE : Constants.TWO;
@@ -493,6 +533,7 @@
            changeVersion.setUpdateTime(now);
            if (Constants.equalsInteger(newAuditStatus, Constants.ONE)) {
                changeVersion.setRevenueShareConfig(revenueShareConfig);
                validateCityAndSetDeposit(changeVersion);
            }
            shopInfoMapper.updateById(changeVersion);
@@ -506,6 +547,7 @@
                official.setUpdateTime(now);
                if (Constants.equalsInteger(newAuditStatus, Constants.ONE)) {
                    official.setDepositAmount(changeVersion.getDepositAmount());
                    official.setRevenueShareConfig(revenueShareConfig);
                }
                shopInfoMapper.updateById(official);
            }
@@ -520,8 +562,19 @@
                changeVersion.setAuditTime(now);
                changeVersion.setAuditRemark(auditDTO.getAuditRemark());
                changeVersion.setAuditUserId(auditDTO.getAuditUser());
                changeVersion.setRevenueShareConfig(revenueShareConfig);
                changeVersion.setUpdateTime(now);
                shopInfoMapper.updateById(changeVersion);
                // æ ‡è®°åŽ†å²çš„å˜æ›´ç‰ˆæœ¬ä¸ºåˆ é™¤
                shopInfoMapper.update(null, new UpdateWrapper<ShopInfo>().lambda()
                        .eq(ShopInfo::getRegionMemberId, changeVersion.getRegionMemberId())
                        .eq(ShopInfo::getVersionType, Constants.ONE)
                        .eq(ShopInfo::getDeleted, Constants.ZERO)
                        .ne(ShopInfo::getId, changeVersion.getId())
                        .ne(ShopInfo::getId, officialId)
                        .set(ShopInfo::getDeleted, Constants.ONE)
                        .set(ShopInfo::getUpdateTime, now));
                // åŒæ­¥å˜æ›´ç‰ˆæœ¬æ•°æ®åˆ°æ­£å¼ç‰ˆæœ¬
                syncChangeToOfficial(changeVersion, official, now);
@@ -725,6 +778,7 @@
        shopInfo.setAliAccount(request.getAliAccount());
        shopInfo.setAliName(request.getAliName());
        shopInfo.setRevenueShareConfig(buildRevenueShareConfig(request.getLocalDeposit(), request.getRemoteDeposit(), request.getRemoteTake()));
        shopInfo.setLocationTagIds(request.getLocationTagIds());
        shopInfo.setUpdateTime(now);
        shopInfoMapper.updateById(shopInfo);
@@ -915,6 +969,19 @@
        vo.setDeliveryRange(shopInfo.getDeliveryArea());
        vo.setVersionType(shopInfo.getVersionType());
        vo.setRelationShopId(shopInfo.getRelationShopId());
        vo.setLocationTagIds(shopInfo.getLocationTagIds());
        // ä½ç½®æ ‡ç­¾åç§°
        if (StringUtils.isNotBlank(shopInfo.getLocationTagIds())) {
            List<String> tagNames = new ArrayList<>();
            for (String tagId : shopInfo.getLocationTagIds().split(",")) {
                Category tag = categoryMapper.selectById(Integer.valueOf(tagId.trim()));
                if (tag != null) {
                    tagNames.add(tag.getName());
                }
            }
            vo.setLocationTagNames(String.join(",", tagNames));
        }
        // è§£æžæ”¶ç›Šæ¯”例配置
        if (StringUtils.isNotBlank(shopInfo.getRevenueShareConfig())) {
            try {
@@ -945,6 +1012,13 @@
        vo.setIdcardImgBackUrl(StringUtils.isNotBlank(shopInfo.getIdcardImgBack()) ? imgPrefix + shopInfo.getIdcardImgBack() : null);
        vo.setBusinessImg(shopInfo.getBusinessImg());
        vo.setBusinessImgUrl(StringUtils.isNotBlank(shopInfo.getBusinessImg()) ? imgPrefix + shopInfo.getBusinessImg() : null);
        vo.setCoverImg(shopInfo.getCoverImg());
        vo.setCoverImgUrl(StringUtils.isNotBlank(shopInfo.getCoverImg()) ? imgPrefix + shopInfo.getCoverImg() : null);
        vo.setContent(shopInfo.getContent());
        vo.setDepositTypes(shopInfo.getDepositTypes());
        vo.setFeeStandard(shopInfo.getFeeStandard());
        vo.setShopHours(shopInfo.getShopHours());
        vo.setBusinessType(shopInfo.getBusinessType());
        // æŸ¥è¯¢é™„ä»¶
        QueryWrapper<Multifile> fileQw = new QueryWrapper<>();
@@ -1046,6 +1120,11 @@
                    "SELECT id FROM areas WHERE parent_id = " + dto.getCityId() + " AND isdeleted = 0");
        }
        // ä½ç½®æ ‡ç­¾ç­›é€‰
        if (dto.getLocationTagId() != null) {
            qw.apply("FIND_IN_SET({0}, LOCATION_TAG_IDS) > 0", dto.getLocationTagId());
        }
        // è·ç¦»ç­›é€‰ï¼ˆå•位:米 â†’ è½¬æ¢ä¸ºkm比较)
        if (distanceMeter != null && distanceMeter > 0) {
            double maxKm = distanceMeter / 1000.0;
@@ -1082,6 +1161,8 @@
            vo.setLongitude(shop.getLongitude());
            // é—¨å¤´ç…§ç¬¬ä¸€å¼ 
            vo.setCoverImg(getFirstImage(shop.getId(), Constants.FileType.STORE_FRONT.getKey(), imgPrefix));
            // ä½ç½®æ ‡ç­¾åç§°
            vo.setLocationTagNames(resolveLocationTagNames(shop.getLocationTagIds()));
            // è·ç¦»
            if (longitude != null && latitude != null && shop.getLongitude() != null && shop.getLatitude() != null) {
                double distKm = haversine(latitude, longitude, shop.getLatitude(), shop.getLongitude());
@@ -1096,6 +1177,53 @@
        pageData.setPage(result.getCurrent());
        pageData.setCapacity(result.getSize());
        return pageData;
    }
    @Override
    public List<LocationTagShopCountVO> countShopsByLocationTag(Integer cityId) {
        // æŸ¥è¯¢æ‰€æœ‰ä½ç½®æ ‡ç­¾
        List<Category> tags = categoryMapper.selectList(new QueryWrapper<Category>().lambda()
                .eq(Category::getDeleted, Constants.ZERO)
                .eq(Category::getStatus, Constants.ZERO)
                .eq(Category::getType, Constants.FIVE)
                .orderByAsc(Category::getSortnum));
        if (tags.isEmpty()) {
            return new ArrayList<>();
        }
        // æž„建门店基础查询条件
        QueryWrapper<ShopInfo> baseQw = new QueryWrapper<>();
        baseQw.lambda()
                .eq(ShopInfo::getDeleted, Constants.ZERO)
                .eq(ShopInfo::getStatus, Constants.ZERO)
                .eq(ShopInfo::getVersionType, Constants.ZERO)
                .eq(ShopInfo::getAuditStatus, Constants.THREE);
        if (cityId != null) {
            baseQw.inSql("AREA_ID",
                    "SELECT id FROM areas WHERE parent_id = " + cityId + " AND isdeleted = 0");
        }
        // æŒ‰æ ‡ç­¾ç»Ÿè®¡
        List<LocationTagShopCountVO> result = new ArrayList<>();
        for (Category tag : tags) {
            QueryWrapper<ShopInfo> qw = baseQw.clone();
            qw.apply("FIND_IN_SET({0}, LOCATION_TAG_IDS) > 0", tag.getId());
            Long count = shopInfoMapper.selectCount(qw);
            LocationTagShopCountVO vo = new LocationTagShopCountVO();
            vo.setTagId(tag.getId());
            vo.setTagName(tag.getName());
            vo.setShopCount(count != null ? count.intValue() : 0);
            result.add(vo);
        }
        // æ€»æ•°æ”¾åœ¨åˆ—表第一个
        Long totalCount = shopInfoMapper.selectCount(baseQw);
        LocationTagShopCountVO totalVo = new LocationTagShopCountVO();
        totalVo.setTagId(null);
        totalVo.setTagName("寄存点总数");
        totalVo.setShopCount(totalCount != null ? totalCount.intValue() : 0);
        result.add(0, totalVo);
        return result;
    }
    @Override
@@ -1115,6 +1243,7 @@
        vo.setLatitude(shop.getLatitude());
        vo.setLongitude(shop.getLongitude());
        vo.setLinkPhone(shop.getLinkPhone());
        vo.setLocationTagNames(resolveLocationTagNames(shop.getLocationTagIds()));
        // é—¨å¤´ç…§ + å†…部照 å…¨è·¯å¾„集合
        String imgPrefix = getShopPrefix();
@@ -1204,6 +1333,18 @@
    /**
     * èŽ·å–é—¨åº—å›¾ç‰‡å‰ç¼€
     */
    private String resolveLocationTagNames(String locationTagIds) {
        if (StringUtils.isBlank(locationTagIds)) return null;
        List<String> tagNames = new ArrayList<>();
        for (String tagId : locationTagIds.split(",")) {
            Category tag = categoryMapper.selectById(Integer.valueOf(tagId.trim()));
            if (tag != null) {
                tagNames.add(tag.getName());
            }
        }
        return tagNames.isEmpty() ? null : String.join(",", tagNames);
    }
    private String getShopPrefix() {
        try {
            return systemDictDataBiz.queryByCode(Constants.OSS, Constants.RESOURCE_PATH).getCode()
@@ -1391,6 +1532,128 @@
        return vo;
    }
    @Override
    public List<LuggageTypeItem> shopLuggageTypeList(Integer shopId, ShopRevenueQueryDTO query) {
        QueryWrapper<Orders> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .in(Orders::getStatus,
                        Constants.OrderStatus.waitDeposit.getKey(),
                        Constants.OrderStatus.deposited.getKey(),
                        Constants.OrderStatus.accepted.getKey(),
                        Constants.OrderStatus.delivering.getKey(),
                        Constants.OrderStatus.arrived.getKey(),
                        Constants.OrderStatus.finished.getKey())
                .and(w -> w.eq(Orders::getDepositShopId, shopId).or().eq(Orders::getTakeShopId, shopId));
        if (query.getStartDate() != null) {
            qw.lambda().ge(Orders::getCreateTime, query.getStartDate());
        }
        if (query.getEndDate() != null) {
            qw.lambda().le(Orders::getCreateTime, Utils.Date.getEnd(query.getEndDate()));
        }
        List<Orders> orders = ordersMapper.selectList(qw);
        List<Integer> orderIds = orders.stream().map(Orders::getId).collect(Collectors.toList());
        return dataBoardService.buildLuggageTypeList(orderIds);
    }
    @Override
    public ShopKpiVO getShopKpi(Integer shopId, ShopRevenueQueryDTO query) {
        // æŸ¥è¯¢é—¨åº—参与的订单(状态1-7,日期范围)
        QueryWrapper<Orders> qw = new QueryWrapper<>();
        qw.lambda()
                .eq(Orders::getDeleted, Constants.ZERO)
                .in(Orders::getStatus,
                        Constants.OrderStatus.waitDeposit.getKey(),
                        Constants.OrderStatus.deposited.getKey(),
                        Constants.OrderStatus.accepted.getKey(),
                        Constants.OrderStatus.delivering.getKey(),
                        Constants.OrderStatus.arrived.getKey(),
                        Constants.OrderStatus.finished.getKey())
                .and(w -> w.eq(Orders::getDepositShopId, shopId).or().eq(Orders::getTakeShopId, shopId));
        if (query.getStartDate() != null) {
            qw.lambda().ge(Orders::getCreateTime, query.getStartDate());
        }
        if (query.getEndDate() != null) {
            qw.lambda().le(Orders::getCreateTime, Utils.Date.getEnd(query.getEndDate()));
        }
        List<Orders> orders = ordersMapper.selectList(qw);
        ShopKpiVO vo = new ShopKpiVO();
        // å¯„存订单量(就地)
        long localCount = orders.stream().filter(o -> Constants.equalsInteger(o.getType(), Constants.ZERO)).count();
        vo.setLocalOrderCount((int) localCount);
        // å¯„送订单量(异地)
        long remoteCount = orders.stream().filter(o -> Constants.equalsInteger(o.getType(), Constants.ONE)).count();
        vo.setRemoteOrderCount((int) remoteCount);
        // æ€»è®¢å•量
        vo.setTotalOrderCount(orders.size());
        // æ€»å®Œæˆè®¢å•量
        long finishedCount = orders.stream()
                .filter(o -> Constants.equalsInteger(o.getStatus(), Constants.OrderStatus.finished.getStatus()))
                .count();
        vo.setFinishedOrderCount((int) finishedCount);
        // æ€»è¥æ”¶é‡‘额 = sum(totalAmount - refundAmount)
        long totalRevenue = orders.stream()
                .mapToLong(o -> {
                    long total = o.getTotalAmount() != null ? o.getTotalAmount() : 0L;
                    long refund = o.getRefundAmount() != null ? o.getRefundAmount() : 0L;
                    return total - refund;
                }).sum();
        vo.setTotalRevenue(totalRevenue);
        // é—¨åº—分成金额
        long shopFee = 0L;
        for (Orders o : orders) {
            if (Constants.equalsInteger(o.getType(), Constants.ZERO)) {
                // å°±åœ°å¯„存:取存件门店分成
                if (Constants.equalsInteger(o.getDepositShopId(), shopId)) {
                    shopFee += o.getDepositShopFee() != null ? o.getDepositShopFee() : 0L;
                }
            } else if (Constants.equalsInteger(o.getType(), Constants.ONE)) {
                // å¼‚地:作为存件门店取depositShopFee,作为取件门店取takeShopFee
                if (Constants.equalsInteger(o.getDepositShopId(), shopId)) {
                    shopFee += o.getDepositShopFee() != null ? o.getDepositShopFee() : 0L;
                }
                if (Constants.equalsInteger(o.getTakeShopId(), shopId)) {
                    shopFee += o.getTakeShopFee() != null ? o.getTakeShopFee() : 0L;
                }
            }
        }
        vo.setShopFeeTotal(shopFee);
        // é€€æ¬¾å•æ•°
        long refundCount = orders.stream()
                .filter(o -> o.getRefundAmount() != null && o.getRefundAmount() > 0)
                .count();
        vo.setRefundOrderCount((int) refundCount);
        // è´£ä»»æ‰£æ¬¾æ€»é¢ï¼šRevenue memberId=shopId, memberType=2, type=4
        QueryWrapper<Revenue> revQw = new QueryWrapper<>();
        revQw.lambda()
                .eq(Revenue::getMemberId, shopId)
                .eq(Revenue::getMemberType, Constants.TWO)
                .eq(Revenue::getType, Constants.FOUR)
                .eq(Revenue::getDeleted, Constants.ZERO);
        if (query.getStartDate() != null) {
            revQw.lambda().ge(Revenue::getCreateTime, query.getStartDate());
        }
        if (query.getEndDate() != null) {
            revQw.lambda().le(Revenue::getCreateTime, Utils.Date.getEnd(query.getEndDate()));
        }
        List<Revenue> deductRecords = revenueMapper.selectList(revQw);
        long deductTotal = deductRecords.stream()
                .mapToLong(r -> r.getAmount() != null ? r.getAmount() : 0L)
                .sum();
        vo.setDeductTotal(deductTotal);
        return vo;
    }
    /**
     * å•†æˆ·è´¦å·å¯†ç ç™»å½•
     * @param dto
server/services/src/main/java/com/doumee/service/common/EmailService.java
@@ -1,9 +1,10 @@
package com.doumee.service.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
@@ -13,67 +14,114 @@
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class EmailService {
    @Autowired
    private JavaMailSender javaMailSender;//注入JavaMailSender
    private JavaMailSender javaMailSender;
    @Value("${spring.mail.username}")
    private String fromEmail;
    public boolean sendEmailWithLocalFiles(String toEmail, String title, String content, List<Map<String,Object>> fileList) {
    /**
     * å‘送纯文本邮件
     */
    public boolean sendText(String toEmail, String subject, String text) {
        try {
                AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
                context.refresh();
                MimeMessage message = javaMailSender.createMimeMessage();
                MimeMessageHelper helper = new MimeMessageHelper(message, true);
                helper.setTo(toEmail);
                helper.setFrom(fromEmail);
                helper.setSubject(title);
                helper.setText(content);
                if(fileList!=null){
                    for (Map<String,Object> f : fileList){
                        // è®¾ç½®é™„ä»¶
                        helper.addAttachment((String) f.get("name"),new FileSystemResource((File) f.get("file")));
                    }
                }
            SimpleMailMessage message = new SimpleMailMessage();
            message.setTo(toEmail);
            message.setFrom(fromEmail);
            message.setSubject(subject);
            message.setText(text);
            javaMailSender.send(message);
                    System.out.println("邮件发送成功!");
                    return true;
            } catch (Exception e) {
            e.printStackTrace();
                    return false;
                }
            }
    public boolean sendEmailWithImages(String toEmail, String title, Map<String,String> contentForm, List<String> imgList) {
        try {
                AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
                context.refresh();
                MimeMessage message = javaMailSender.createMimeMessage();
                MimeMessageHelper helper = new MimeMessageHelper(message, true);
                helper.setTo(toEmail);
                helper.setFrom(fromEmail);
                helper.setSubject(title);
                String content = "<html><body>";
                if(contentForm!=null){
                    for (Map.Entry<String, String> f : contentForm.entrySet()) {
                        // è®¾ç½®é™„ä»¶
                        content += "<div style='display:block;'>"+f.getKey()+":"+f.getValue()+"</p>";
                    }
                }
                if(imgList!=null){
                    content += "<div style='display:block;'> ";
                    for (String f : imgList){
                        // è®¾ç½®é™„ä»¶
                        content += "<a href='"+f+"' target='blank'><img style='width:200px;margin:5px' src='"+f+"'/></a>";
                    }
                }
            content += "</div></body><html>";
            helper.setText(content,true);
            javaMailSender.send(message);
                    System.out.println("邮件发送成功!");
                    return true;
            } catch (Exception e) {
            e.printStackTrace();
                    return false;
                }
            }
            log.info("纯文本邮件发送成功: to={}", toEmail);
            return true;
        } catch (Exception e) {
            log.error("纯文本邮件发送失败: to={}, error={}", toEmail, e.getMessage());
            return false;
        }
    }
    /**
     * å‘送HTML邮件
     */
    public boolean sendHtml(String toEmail, String subject, String html) {
        try {
            MimeMessage message = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true);
            helper.setTo(toEmail);
            helper.setFrom(fromEmail);
            helper.setSubject(subject);
            helper.setText(html, true);
            javaMailSender.send(message);
            log.info("HTML邮件发送成功: to={}", toEmail);
            return true;
        } catch (Exception e) {
            log.error("HTML邮件发送失败: to={}, error={}", toEmail, e.getMessage());
            return false;
        }
    }
    /**
     * å‘送带附件邮件
     * @param fileList é™„件列表,每个Map包含 name(文件名) å’Œ file(File对象)
     */
    public boolean sendWithAttachment(String toEmail, String subject, String text, List<Map<String, Object>> fileList) {
        try {
            MimeMessage message = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true);
            helper.setTo(toEmail);
            helper.setFrom(fromEmail);
            helper.setSubject(subject);
            helper.setText(text);
            if (fileList != null) {
                for (Map<String, Object> f : fileList) {
                    helper.addAttachment((String) f.get("name"), new FileSystemResource((File) f.get("file")));
                }
            }
            javaMailSender.send(message);
            log.info("附件邮件发送成功: to={}", toEmail);
            return true;
        } catch (Exception e) {
            log.error("附件邮件发送失败: to={}, error={}", toEmail, e.getMessage());
            return false;
        }
    }
    /**
     * å‘送带内嵌图片的HTML邮件
     * @param contentForm è¡¨å•内容键值对
     * @param imgList     å›¾ç‰‡URL列表
     */
    public boolean sendWithInlineImage(String toEmail, String subject, Map<String, String> contentForm, List<String> imgList) {
        try {
            MimeMessage message = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true);
            helper.setTo(toEmail);
            helper.setFrom(fromEmail);
            helper.setSubject(subject);
            StringBuilder content = new StringBuilder("<html><body>");
            if (contentForm != null) {
                for (Map.Entry<String, String> entry : contentForm.entrySet()) {
                    content.append("<div style='display:block;'>").append(entry.getKey()).append(":").append(entry.getValue()).append("</div>");
                }
            }
            if (imgList != null) {
                content.append("<div style='display:block;'>");
                for (String img : imgList) {
                    content.append("<a href='").append(img).append("' target='blank'><img style='width:200px;margin:5px' src='").append(img).append("'/></a>");
                }
                content.append("</div>");
            }
            content.append("</body></html>");
            helper.setText(content.toString(), true);
            javaMailSender.send(message);
            log.info("图片邮件发送成功: to={}", toEmail);
            return true;
        } catch (Exception e) {
            log.error("图片邮件发送失败: to={}, error={}", toEmail, e.getMessage());
            return false;
        }
    }
}
server/services/src/main/resources/application-dev.yml
@@ -34,17 +34,21 @@
#            enable: true
#            required: true
  mail:
    host: smtp.exmail.qq.com
    username: jp@doumee.com
    password: 2Jz9HFW2U7vRnCRu
    host: smtp.qq.com
    port: 465
    username: 1171472578@qq.com
    password: bxecisqcmdauibgb
    default-encoding: UTF-8
    properties:
      mail:
        smtp:
          auth: true
        starttls:
          enable: true
          required: true
          ssl:
            enable: true
            trust: smtp.qq.com
            socketFactory:
              class: javax.net.ssl.SSLSocketFactory
              port: 465
debug_model: true
server/services/src/main/resources/application-pro.yml
@@ -16,19 +16,21 @@
    time-zone: GMT+8
    date-format: yyyy-MM-dd HH:mm:ss
  mail:
    host: smtp.aliyun.com
    host: smtp.qq.com
    port: 465
    username: your_email@aliyun.com
    password: your_password
    username: 1171472578@qq.com
    password: bxecisqcmdauibgb
    default-encoding: UTF-8
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true
          ssl:
            enable: true
            trust: smtp.qq.com
            socketFactory:
              class: javax.net.ssl.SSLSocketFactory
              port: 465
debug_model: false
########################极光推送配置########################
jpush:
server/services/src/main/resources/application-test.yml
@@ -34,17 +34,21 @@
#            enable: true
#            required: true
  mail:
    host: smtp.exmail.qq.com
    username: jp@doumee.com
    password: 2Jz9HFW2U7vRnCRu
    host: smtp.qq.com
    port: 465
    username: 1171472578@qq.com
    password: bxecisqcmdauibgb
    default-encoding: UTF-8
    properties:
      mail:
        smtp:
          auth: true
        starttls:
          enable: true
          required: true
          ssl:
            enable: true
            trust: smtp.qq.com
            socketFactory:
              class: javax.net.ssl.SSLSocketFactory
              port: 465
debug_model: true
server/web/src/main/java/com/doumee/api/web/ConfigApi.java
@@ -164,6 +164,7 @@
    @ApiOperation(value = "计算就地存取预估费用", notes = "根据城市、天数、物品类型和数量计算就地存取预估费用")
    @PostMapping("/calculateLocalPrice")
    public ApiResponse<PriceCalculateVO> calculateLocalPrice(@RequestBody @Valid CalculateLocalPriceDTO dto) {
        dto.setMemberId(getMemberId());
        return ApiResponse.success("操作成功", ordersService.calculateLocalPrice(dto));
    }
@@ -171,6 +172,7 @@
    @ApiOperation(value = "计算异地存取预估费用", notes = "根据距离、物品类型和数量计算异地存取预估费用")
    @PostMapping("/calculateRemotePrice")
    public ApiResponse<PriceCalculateVO> calculateRemotePrice(@RequestBody @Valid CalculateRemotePriceDTO dto) {
        dto.setMemberId(getMemberId());
        return ApiResponse.success("操作成功", ordersService.calculateRemotePrice(dto));
    }
server/web/src/main/java/com/doumee/api/web/InvoiceApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
package com.doumee.api.web;
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.InvoiceRecord;
import com.doumee.dao.dto.ApplyInvoiceDTO;
import com.doumee.dao.dto.SendInvoiceEmailDTO;
import com.doumee.service.business.InvoiceRecordService;
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.*;
import javax.validation.Valid;
/**
 * è®¢å•发票
 *
 * @Author : Rk
 * @create 2026/5/18 9:42
 */
@Api(tags = "订单发票")
@Trace(exclude = true)
@RestController
@RequestMapping("/web/invoice")
@Slf4j
public class InvoiceApi extends ApiController {
    @Autowired
    private InvoiceRecordService invoiceRecordService;
    @LoginRequired
    @ApiOperation("开票申请")
    @PostMapping("/apply")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true)
    })
    public ApiResponse<String> apply(@RequestBody @Valid ApplyInvoiceDTO dto) {
        invoiceRecordService.applyInvoice(dto, getMemberId());
        return ApiResponse.success("申请成功");
    }
    @LoginRequired
    @ApiOperation("开票记录分页")
    @PostMapping("/page")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true)
    })
    public ApiResponse<PageData<InvoiceRecord>> page(@RequestBody @Validated PageWrap<InvoiceRecord> pageWrap) {
        return ApiResponse.success("查询成功", invoiceRecordService.findMemberInvoicePage(pageWrap, getMemberId()));
    }
    @LoginRequired
    @ApiOperation("发送发票邮件")
    @PostMapping("/sendEmail")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true)
    })
    public ApiResponse<String> sendEmail(@RequestBody @Valid SendInvoiceEmailDTO dto) {
        invoiceRecordService.sendInvoiceEmail(getMemberId(), dto.getInvoiceRecordId(), dto.getEmail());
        return ApiResponse.success("发送成功");
    }
}
server/web/src/main/java/com/doumee/api/web/MemberCouponApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package com.doumee.api.web;
import com.doumee.core.annotation.LoginRequired;
import com.doumee.core.model.ApiResponse;
import com.doumee.core.model.PageData;
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.MemberCoupon;
import com.doumee.service.business.MemberCouponService;
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.web.bind.annotation.*;
@Slf4j
@Api(tags = "会员优惠券")
@RestController
@RequestMapping("/web/memberCoupon")
public class MemberCouponApi extends ApiController {
    @Autowired
    private MemberCouponService memberCouponService;
    @LoginRequired
    @ApiOperation(value = "会员优惠券列表", notes = "小程序端")
    @PostMapping("/findPage")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
    })
    public ApiResponse<PageData<MemberCoupon>> findPage(@RequestBody PageWrap<MemberCoupon> pageWrap) {
        Integer status = pageWrap.getModel() != null ? pageWrap.getModel().getStatus() : null;
        return ApiResponse.success("操作成功", memberCouponService.findMemberPage(getMemberId(), status, pageWrap));
    }
    @LoginRequired
    @ApiOperation(value = "领取优惠券", notes = "小程序端")
    @GetMapping("/claim")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
    })
    public ApiResponse claim(@RequestParam Integer couponId) {
        memberCouponService.claimCoupon(getMemberId(), couponId);
        return ApiResponse.success("操作成功");
    }
}
server/web/src/main/java/com/doumee/api/web/OrdersApi.java
@@ -216,5 +216,4 @@
        return ApiResponse.success("确认收货成功");
    }
}
server/web/src/main/java/com/doumee/api/web/PaymentCallback.java
@@ -201,6 +201,15 @@
                    ordersRefundMapper.updateById(refundRecord);
                    log.info("退款记录状态已更新, refundRecordId={}, status={}", refundRecord.getId(), refundRecord.getStatus());
                    // æ‰‹åŠ¨é€€æ¬¾(type=4)退款成功 â†’ æ‰§è¡Œæ‰£æ¬¾
                    if (Status.SUCCESS.equals(refundStatus) && Constants.equalsInteger(refundRecord.getType(), Constants.FOUR)) {
                        try {
                            ordersService.processManualRefundCallback(refundRecord);
                        } catch (Exception ex) {
                            log.error("手动退款扣款处理异常, refundRecordId={}", refundRecord.getId(), ex);
                        }
                    }
                    // é€€æ¬¾æˆåŠŸ â†’ é€šçŸ¥ä¼šå‘˜
                    if (Status.SUCCESS.equals(refundStatus) && refundRecord.getOrderId() != null) {
                        Orders refundOrder = ordersMapper.selectById(refundRecord.getOrderId());
server/web/src/main/java/com/doumee/api/web/RevenueApi.java
@@ -9,8 +9,16 @@
import com.doumee.core.model.PageWrap;
import com.doumee.dao.business.model.Revenue;
import com.doumee.dao.dto.RevenueQueryDTO;
import com.doumee.dao.dto.ShopRevenueQueryDTO;
import com.doumee.dao.vo.LuggageTypeItem;
import com.doumee.dao.vo.RevenueSummaryVO;
import com.doumee.dao.vo.ShopKpiVO;
import com.doumee.dao.vo.DriverKpiVO;
import com.doumee.dao.vo.DriverOrderTrendVO;
import com.doumee.dao.vo.DriverRewardHallVO;
import com.doumee.dao.dto.ClaimRewardDTO;
import com.doumee.service.business.RevenueService;
import com.doumee.service.business.ShopInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
@@ -19,6 +27,9 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@Api(tags = "交易流水")
@Trace(exclude = true)
@@ -29,6 +40,9 @@
    @Autowired
    private RevenueService revenueService;
    @Autowired
    private ShopInfoService shopInfoService;
    @LoginDriverRequired
    @ApiOperation(value = "司机流水分页", notes = "小程序端")
@@ -70,4 +84,65 @@
        return ApiResponse.success("查询成功", revenueService.getShopRevenueSummary(queryDTO, getShopId()));
    }
    @LoginShopRequired
    @ApiOperation("门店行李类型分布")
    @PostMapping("/shopLuggageType")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "门店token值", required = true),
    })
    public ApiResponse<List<LuggageTypeItem>> shopLuggageType(@RequestBody @Valid ShopRevenueQueryDTO query) {
        return ApiResponse.success("查询成功", shopInfoService.shopLuggageTypeList(getShopId(), query));
    }
    @LoginShopRequired
    @ApiOperation("门店核心业绩指标")
    @PostMapping("/shopKpi")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "门店token值", required = true),
    })
    public ApiResponse<ShopKpiVO> shopKpi(@RequestBody @Valid ShopRevenueQueryDTO query) {
        return ApiResponse.success("查询成功", shopInfoService.getShopKpi(getShopId(), query));
    }
    @LoginDriverRequired
    @ApiOperation("司机核心业绩指标")
    @PostMapping("/driverKpi")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
    })
    public ApiResponse<DriverKpiVO> driverKpi(@RequestBody @Valid ShopRevenueQueryDTO query) {
        return ApiResponse.success("查询成功", revenueService.getDriverKpi(getDriverId(), query));
    }
    @LoginDriverRequired
    @ApiOperation("司机近七日订单趋势")
    @PostMapping("/driverOrderTrend")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
    })
    public ApiResponse<List<DriverOrderTrendVO>> driverOrderTrend() {
        return ApiResponse.success("查询成功", revenueService.getDriverOrderTrend(getDriverId()));
    }
    @LoginDriverRequired
    @ApiOperation("司机奖励大厅")
    @PostMapping("/driverRewardHall")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
    })
    public ApiResponse<DriverRewardHallVO> driverRewardHall() {
        return ApiResponse.success("查询成功", revenueService.getDriverRewardHall(getDriverId()));
    }
    @LoginDriverRequired
    @ApiOperation("领取奖励金")
    @PostMapping("/claimReward")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "header", dataType = "String", name = "token", value = "用户token值", required = true),
    })
    public ApiResponse<String> claimReward(@RequestBody @Valid ClaimRewardDTO dto) {
        revenueService.claimReward(getDriverId(), dto.getRewardRecordId());
        return ApiResponse.success("领取成功");
    }
}
server/web/src/main/java/com/doumee/api/web/ShopInfoApi.java
@@ -15,6 +15,7 @@
import com.doumee.dao.dto.ShopNearbyDTO;
import com.doumee.dao.vo.ShopDetailVO;
import com.doumee.dao.vo.ShopCenterVO;
import com.doumee.dao.vo.LocationTagShopCountVO;
import com.doumee.dao.vo.ShopNearbyVO;
import com.doumee.dao.vo.ShopSalesStatsVO;
import com.doumee.dao.vo.ShopWebDetailVO;
@@ -29,6 +30,8 @@
import javax.validation.Valid;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * é—¨åº—入驻(小程序端)
@@ -69,6 +72,13 @@
        return ApiResponse.success(shopInfoService.findNearbyShops(pageWrap));
    }
    @ApiOperation("按位置标签统计门店数量")
    @GetMapping("/locationTagShopCount")
    public ApiResponse<List<LocationTagShopCountVO>> locationTagShopCount(
            @RequestParam(required = false) Integer cityId) {
        return ApiResponse.success(shopInfoService.countShopsByLocationTag(cityId));
    }
    @ApiOperation("门店详情")
    @PostMapping("/detail")
    public ApiResponse<ShopWebDetailVO> detail(@RequestBody @Validated ShopDetailQueryDTO dto) {