浏览代码

Merge branch 'feature/service-vpp-20260701' of http://47.111.81.118:3000/uskycloud/usky-modules into feature/service-vpp-20260701

hanzhengyi 5 天之前
父节点
当前提交
3f2c52a221

+ 11 - 0
service-vpp/service-vpp-biz/pom.xml

@@ -92,6 +92,17 @@
             <version>0.0.1</version>
         </dependency>
 
+        <!-- Apache POI(Excel 导出) -->
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi</artifactId>
+            <version>4.1.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+            <version>4.1.2</version>
+        </dependency>
         <!-- 阿里云短信(需求响应邀约通知) -->
         <dependency>
             <groupId>com.aliyun</groupId>

+ 189 - 23
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/controller/web/CustomerController.java

@@ -2,21 +2,29 @@ package com.usky.vpp.controller.web;
 
 import com.usky.common.core.bean.ApiResult;
 import com.usky.common.core.bean.CommonPage;
-import com.usky.vpp.domain.VppCustomer;
-import com.usky.vpp.domain.VppCustomerAccess;
 import com.usky.vpp.domain.VppCustomerContact;
 import com.usky.vpp.service.VppCustomerService;
 import com.usky.vpp.service.vo.CustomerAccessAuditRequest;
-import com.usky.vpp.service.vo.CustomerAccessRequest;
+import com.usky.vpp.service.vo.CustomerAccessRequestVO;
+import com.usky.vpp.service.vo.CustomerAccessResponseVO;
 import com.usky.vpp.service.vo.CustomerContactRequest;
+import com.usky.vpp.service.vo.CustomerRequestVO;
+import com.usky.vpp.service.vo.CustomerResponseVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.Map;
+import javax.servlet.http.HttpServletResponse;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 
 /**
  * 客户管理接口
+ * <p>分为两大类:准入管理 和 客户管理</p>
  * 网关前缀: /prod-api/service-vpp/customer
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
  */
 @RestController
 @RequestMapping("/customer")
@@ -25,49 +33,203 @@ public class CustomerController {
     @Autowired
     private VppCustomerService vppCustomerService;
 
+    // ==================== 准入管理 ====================
+
+    /**
+     * 提交准入申请
+     */
     @PostMapping("/access")
-    public ApiResult<VppCustomerAccess> submitAccess(@RequestBody CustomerAccessRequest body) {
-        return ApiResult.success(vppCustomerService.submitAccess(body));
+    public ApiResult<Boolean> submitAccess(@RequestBody CustomerAccessRequestVO vo) {
+        return vppCustomerService.submitAccess(vo)
+                ? ApiResult.success(true)
+                : ApiResult.error("提交准入申请失败,请重试!");
     }
 
+    /**
+     * 分页查询准入申请
+     * <ul>
+     *   <li>id           主键ID 精确匹配</li>
+     *   <li>accessStatus 准入状态 0待审核 1通过 2拒绝</li>
+     *   <li>customerName 客户名称 模糊匹配</li>
+     *   <li>current      页码</li>
+     *   <li>size         页大小</li>
+     * </ul>
+     */
     @GetMapping("/access")
-    public ApiResult<CommonPage<VppCustomerAccess>> pageAccess(@RequestParam(required = false) Map<String, Object> params) {
-        return ApiResult.success(vppCustomerService.pageAccess(params));
+    public ApiResult<CommonPage<CustomerAccessResponseVO>> pageAccess(
+            @RequestParam(value = "id", required = false) Long id,
+            @RequestParam(value = "accessStatus", required = false) Integer accessStatus,
+            @RequestParam(value = "customerName", required = false) String customerName,
+            @RequestParam(value = "startTime", required = false) String startTime,
+            @RequestParam(value = "endTime", required = false) String endTime,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        LocalDateTime start = startTime != null && !startTime.isEmpty()
+                ? LocalDateTime.parse(startTime, formatter) : null;
+        LocalDateTime end = endTime != null && !endTime.isEmpty()
+                ? LocalDateTime.parse(endTime, formatter) : null;
+        return ApiResult.success(vppCustomerService.pageAccess(id, accessStatus, customerName, start, end, current, size));
     }
 
-    @PutMapping("/access/{id}/audit")
-    public ApiResult<Void> auditAccess(@PathVariable("id") Long id, @RequestBody CustomerAccessAuditRequest body) {
-        vppCustomerService.auditAccess(id, body);
+    /**
+     * 审核准入申请(通过/拒绝)
+     */
+    @PutMapping("/access/audit")
+    public ApiResult<Void> auditAccess(@RequestBody CustomerAccessAuditRequest body) {
+        vppCustomerService.auditAccess(body);
         return ApiResult.success();
     }
 
+    /**
+     * 删除准入申请(软删除)
+     */
+    @DeleteMapping("/access/{id}")
+    public ApiResult<Boolean> deleteAccess(@PathVariable("id") Long id) {
+        return vppCustomerService.deleteAccess(id)
+                ? ApiResult.success(true)
+                : ApiResult.error("删除准入申请失败,请重试!");
+    }
+
+    /**
+     * 导出准入申请数据(Excel)
+     * <ul>
+     *   <li>scope = "current":导出当前页,需传 current + size</li>
+     *   <li>scope = "all":导出当前租户全部未删除准入申请</li>
+     *   <li>scope = "filtered":导出筛选结果,需至少传一个筛选条件</li>
+     * </ul>
+     */
+    @GetMapping("/access/export")
+    public void exportAccess(
+            @RequestParam("scope") String scope,
+            @RequestParam("fileName") String fileName,
+            @RequestParam(value = "id", required = false) Long id,
+            @RequestParam(value = "accessStatus", required = false) Integer accessStatus,
+            @RequestParam(value = "customerName", required = false) String customerName,
+            @RequestParam(value = "startTime", required = false) String startTime,
+            @RequestParam(value = "endTime", required = false) String endTime,
+            @RequestParam(value = "current", required = false) Integer current,
+            @RequestParam(value = "size", required = false) Integer size,
+            HttpServletResponse response) {
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        LocalDateTime start = startTime != null && !startTime.isEmpty()
+                ? LocalDateTime.parse(startTime, formatter) : null;
+        LocalDateTime end = endTime != null && !endTime.isEmpty()
+                ? LocalDateTime.parse(endTime, formatter) : null;
+        vppCustomerService.exportAccess(scope, fileName, id, accessStatus, customerName,
+                start, end, current, size, response);
+    }
+
+    // ==================== 客户管理 ====================
+
+    /**
+     * 直接新增客户(跳过准入流程)
+     */
+    @PostMapping
+    public ApiResult<Boolean> createCustomer(@RequestBody CustomerRequestVO vo) {
+        return vppCustomerService.createCustomer(vo)
+                ? ApiResult.success(true)
+                : ApiResult.error("新增客户失败,请重试!");
+    }
+
+    /**
+     * 分页查询客户(含关联合同)
+     * <ul>
+     *   <li>id               主键ID 精确匹配</li>
+     *   <li>customerName     客户名称 模糊匹配</li>
+     *   <li>accountNo        电力户号 精确匹配</li>
+     *   <li>lifecycleStatus  签约状态</li>
+     *   <li>customerType     客户类型</li>
+     *   <li>current          页码</li>
+     *   <li>size             页大小</li>
+     * </ul>
+     */
     @GetMapping
-    public ApiResult<CommonPage<VppCustomer>> pageCustomer(@RequestParam(required = false) Map<String, Object> params) {
-        return ApiResult.success(vppCustomerService.pageCustomer(params));
+    public ApiResult<CommonPage<CustomerResponseVO>> pageCustomer(
+            @RequestParam(value = "id", required = false) Long id,
+            @RequestParam(value = "customerName", required = false) String customerName,
+            @RequestParam(value = "accountNo", required = false) String accountNo,
+            @RequestParam(value = "lifecycleStatus", required = false) Integer lifecycleStatus,
+            @RequestParam(value = "customerType", required = false) Integer customerType,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        return ApiResult.success(vppCustomerService.pageCustomer(
+                id, customerName, accountNo, lifecycleStatus, customerType, current, size));
     }
 
+    /**
+     * 查询客户详情(含关联合同)
+     */
     @GetMapping("/{id}")
-    public ApiResult<VppCustomer> getCustomer(@PathVariable("id") Long id) {
+    public ApiResult<CustomerResponseVO> getCustomer(@PathVariable("id") Long id) {
         return ApiResult.success(vppCustomerService.getCustomer(id));
     }
 
-    @PutMapping("/{id}")
-    public ApiResult<Void> updateCustomer(@PathVariable("id") Long id, @RequestBody VppCustomer body) {
-        vppCustomerService.updateCustomer(id, body);
-        return ApiResult.success();
+    /**
+     * 更新客户信息
+     */
+    @PutMapping()
+    public ApiResult<Boolean> updateCustomer(@RequestBody CustomerRequestVO vo) {
+        return vppCustomerService.updateCustomer(vo)
+                ? ApiResult.success(true)
+                : ApiResult.error("更新客户信息失败,请重试!");
+    }
+
+    /**
+     * 删除客户(软删除)
+     */
+    @DeleteMapping("/{id}")
+    public ApiResult<Boolean> deleteCustomer(@PathVariable("id") Long id) {
+        return vppCustomerService.deleteCustomer(id)
+                ? ApiResult.success(true)
+                : ApiResult.error("删除客户失败,请重试!");
     }
 
+    /**
+     * 导出客户数据(Excel)
+     * <ul>
+     *   <li>scope = "current":导出当前页,需传 current + size</li>
+     *   <li>scope = "all":导出当前租户全部未删除客户</li>
+     *   <li>scope = "filtered":导出筛选结果,需至少传一个筛选条件</li>
+     * </ul>
+     */
+    @GetMapping("/export")
+    public void exportCustomers(
+            @RequestParam("scope") String scope,
+            @RequestParam("fileName") String fileName,
+            @RequestParam(value = "customerName", required = false) String customerName,
+            @RequestParam(value = "lifecycleStatus", required = false) Integer lifecycleStatus,
+            @RequestParam(value = "current", required = false) Integer current,
+            @RequestParam(value = "size", required = false) Integer size,
+            HttpServletResponse response) {
+        vppCustomerService.exportCustomers(scope, fileName, customerName, lifecycleStatus, current, size, response);
+    }
+
+    // ==================== 联系人管理 ====================
+
+    /**
+     * 分页查询联系人
+     */
     @GetMapping("/{id}/contact")
-    public ApiResult<CommonPage<VppCustomerContact>> listContact(@PathVariable("id") Long id,
-                                                                  @RequestParam(required = false) Map<String, Object> params) {
-        return ApiResult.success(vppCustomerService.listContact(id, params));
+    public ApiResult<CommonPage<VppCustomerContact>> listContact(
+            @PathVariable("id") Long id,
+            @RequestParam(value = "current", required = false, defaultValue = "1") Integer current,
+            @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
+        return ApiResult.success(vppCustomerService.listContact(id, current, size));
     }
 
+    /**
+     * 新增联系人
+     */
     @PostMapping("/{id}/contact")
-    public ApiResult<VppCustomerContact> addContact(@PathVariable("id") Long id, @RequestBody CustomerContactRequest body) {
+    public ApiResult<VppCustomerContact> addContact(@PathVariable("id") Long id,
+                                                    @RequestBody CustomerContactRequest body) {
         return ApiResult.success(vppCustomerService.addContact(id, body));
     }
 
+    /**
+     * 更新联系人
+     */
     @PutMapping("/{id}/contact/{contactId}")
     public ApiResult<Void> updateContact(@PathVariable("id") Long id,
                                          @PathVariable("contactId") Long contactId,
@@ -76,8 +238,12 @@ public class CustomerController {
         return ApiResult.success();
     }
 
+    /**
+     * 删除联系人
+     */
     @DeleteMapping("/{id}/contact/{contactId}")
-    public ApiResult<Void> deleteContact(@PathVariable("id") Long id, @PathVariable("contactId") Long contactId) {
+    public ApiResult<Void> deleteContact(@PathVariable("id") Long id,
+                                         @PathVariable("contactId") Long contactId) {
         vppCustomerService.deleteContact(id, contactId);
         return ApiResult.success();
     }

+ 111 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomer.java

@@ -4,11 +4,15 @@ 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 lombok.Data;
 import lombok.EqualsAndHashCode;
+
+import javax.validation.constraints.NotNull;
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.time.LocalTime;
 
 /**
  * vpp_customer
@@ -26,24 +30,62 @@ public class VppCustomer implements Serializable {
     private String accountNo;
     @TableField("customer_name")
     private String customerName;
+
+    /**
+     * 1中高压 2低压商用 3居民充电桩 4自有资产
+     */
     @TableField("customer_type")
     private Integer customerType;
     @TableField("power_company")
     private String powerCompany;
     @TableField("contract_capacity")
     private BigDecimal contractCapacity;
+
+    /**
+     * 信用状态
+     * 1正常 2关注 3不良
+     */
     @TableField("credit_status")
     private Integer creditStatus;
+
+    /**
+     * 签约状态
+     * 1待签约 2已签约 3履约中 4待续约 5已续约 6已解约
+     */
     @TableField("lifecycle_status")
     private Integer lifecycleStatus;
+
+    /**
+     * 需求响应提前通知分钟数,默认30
+     */
     @TableField("dr_notify_minutes")
     private Integer drNotifyMinutes;
+
+    /**
+     * 登记上调能力 kW
+     */
     @TableField("dr_up_capacity_kw")
     private BigDecimal drUpCapacityKw;
+
+    /**
+     * 需求响应下调能力 kW
+     */
     @TableField("dr_down_capacity_kw")
     private BigDecimal drDownCapacityKw;
+
+    /**
+     * 省
+     */
     private String province;
+
+    /**
+     * 市
+     */
     private String city;
+
+    /**
+     * 区/县
+     */
     private String district;
     private String address;
     @TableField("business_license_url")
@@ -63,4 +105,73 @@ public class VppCustomer implements Serializable {
     private Integer deleteFlag;
     @TableField("deleted_at")
     private LocalDateTime deletedAt;
+
+    /**
+     * 是否虚拟电厂资源(0:否,1:是)
+     */
+    @NotNull(message = "是否虚拟电厂资源不能为空!")
+    @TableField("is_vpp_resource")
+    private Integer isVppResource;
+
+    /**
+     * 虚拟电厂分类(1:充换电,2:分布式三联供,3:楼宇空调,5:用户侧储能,6:数据中心,7:工业负荷,8:其他)
+     */
+    @TableField("vpp_category")
+    private String vppCategory;
+
+    /**
+     * 需求响应资源分类(1:工业负荷,2:商业负荷,3:储能,4:电动汽车,5:居民负荷,6:其他)
+     */
+    @TableField("dr_resource_type")
+    private String drResourceType;
+
+    /**
+     * 聚合代理用户虚拟电厂编码
+     */
+    @TableField("vpp_proxy_code")
+    private String vppProxyCode;
+
+    /**
+     * 所属行业(1:制造业,2:建筑业,3:批发和零售业,4:交通运输、仓储和邮政业,5:住宿和餐饮业,6:信息传输、软件和信息技术服务业,7:金融业,8:房地产业,9:租赁和商务服务业,10:科学研究和技术服务业,
+     * 11:水利、环境和公共设施管理业,12:居民服务、修理和其他服务业,13:教育,14:卫生和社会工作,15:文化、体育和娱乐业,16:公共管理、社会保障和社会组织,17:国际组织,18:农、林、牧、渔业,
+     * 19:采矿业,20:电力、热力、燃气及水生产和供应业,21:其他)
+     */
+    @TableField("industry")
+    private String industry;
+
+    /**
+     * 所属产业(1:第一产业,2:第二产业,3:第三产业)
+     */
+    @TableField("industry_sector")
+    private String industrySector;
+
+    /**
+     * 运行容量(kW)
+     */
+    private BigDecimal runningCapacity;
+
+    /**
+     * 生产经营开始时间
+     */
+    @JsonFormat(pattern = "HH:mm")
+    private LocalTime productionStartDate;
+
+    /**
+     * 生产经营结束时间
+     */
+    @JsonFormat(pattern = "HH:mm")
+    private LocalTime productionEndDate;
+
+    /**
+     * 用电地址
+     */
+    @TableField("power_address")
+    private String powerAddress;
+
+    /**
+     * 签约时间
+     */
+    @TableField("signing_date")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime signingDate;
 }

+ 17 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/domain/VppCustomerAccess.java

@@ -4,8 +4,10 @@ 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 lombok.Data;
 import lombok.EqualsAndHashCode;
+
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
@@ -46,13 +48,28 @@ public class VppCustomerAccess implements Serializable {
     private LocalDateTime auditAt;
     @TableField("customer_id")
     private Long customerId;
+
+    @TableField(exist = false)
+    private LocalDateTime signingDate;
+
+    @TableField(exist = false)
+    private String contactName;
+
+    @TableField(exist = false)
+    private String contactPhone;
+
+    @TableField(exist = false)
+    private String contactEmail;
+
     @TableField("apply_at")
     private LocalDateTime applyAt;
     @TableField("tenant_id")
     private Integer tenantId;
     @TableField("create_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime createTime;
     @TableField("update_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private LocalDateTime updateTime;
     @TableField("created_by")
     private String createdBy;

+ 103 - 11
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/VppCustomerService.java

@@ -1,37 +1,129 @@
 package com.usky.vpp.service;
 
 import com.usky.common.core.bean.CommonPage;
-import com.usky.vpp.domain.VppCustomer;
-import com.usky.vpp.domain.VppCustomerAccess;
 import com.usky.vpp.domain.VppCustomerContact;
 import com.usky.vpp.service.vo.CustomerAccessAuditRequest;
-import com.usky.vpp.service.vo.CustomerAccessRequest;
+import com.usky.vpp.service.vo.CustomerAccessRequestVO;
+import com.usky.vpp.service.vo.CustomerAccessResponseVO;
 import com.usky.vpp.service.vo.CustomerContactRequest;
+import com.usky.vpp.service.vo.CustomerRequestVO;
+import com.usky.vpp.service.vo.CustomerResponseVO;
 
-import java.util.Map;
+import javax.servlet.http.HttpServletResponse;
+import java.time.LocalDateTime;
 
 /**
  * 客户管理业务接口
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
  */
 public interface VppCustomerService {
 
-    VppCustomerAccess submitAccess(CustomerAccessRequest request);
+    // ==================== 准入管理 ====================
 
-    CommonPage<VppCustomerAccess> pageAccess(Map<String, Object> params);
+    /**
+     * 提交准入申请
+     */
+    Boolean submitAccess(CustomerAccessRequestVO request);
 
-    void auditAccess(Long id, CustomerAccessAuditRequest request);
+    /**
+     * 分页查询准入申请
+     */
+    CommonPage<CustomerAccessResponseVO> pageAccess(Long id, Integer accessStatus, String customerName,
+                                                    LocalDateTime startTime, LocalDateTime endTime,
+                                                    Integer pageNum, Integer pageSize);
 
-    CommonPage<VppCustomer> pageCustomer(Map<String, Object> params);
+    /**
+     * 审核准入申请
+     */
+    void auditAccess(CustomerAccessAuditRequest request);
 
-    VppCustomer getCustomer(Long id);
+    /**
+     * 删除准入申请(软删除)
+     */
+    Boolean deleteAccess(Long id);
 
-    void updateCustomer(Long id, VppCustomer customer);
+    /**
+     * 导出准入申请数据(Excel)
+     *
+     * @param scope        导出范围:current / all / filtered
+     * @param fileName     文件名(不含扩展名)
+     * @param id           申请ID精确匹配(filtered 模式可用)
+     * @param accessStatus 准入状态(filtered 模式可用)
+     * @param customerName 客户名称模糊匹配(filtered 模式可用)
+     * @param startTime    申请时间起(filtered 模式可用)
+     * @param endTime      申请时间止(filtered 模式可用)
+     * @param current      当前页码(current 模式必传)
+     * @param size         页大小(current 模式必传)
+     */
+    void exportAccess(String scope, String fileName, Long id, Integer accessStatus,
+                      String customerName, LocalDateTime startTime, LocalDateTime endTime,
+                      Integer current, Integer size, HttpServletResponse response);
 
-    CommonPage<VppCustomerContact> listContact(Long customerId, Map<String, Object> params);
+    // ==================== 客户管理 ====================
 
+    /**
+     * 直接新增客户(跳过准入流程)
+     */
+    Boolean createCustomer(CustomerRequestVO vo);
+
+    /**
+     * 分页查询客户(含关联合同)
+     */
+    CommonPage<CustomerResponseVO> pageCustomer(Long id, String customerName, String accountNo,
+                                                Integer lifecycleStatus, Integer customerType,
+                                                Integer pageNum, Integer pageSize);
+
+    /**
+     * 查询客户详情(含关联合同)
+     */
+    CustomerResponseVO getCustomer(Long id);
+
+    /**
+     * 更新客户信息
+     */
+    Boolean updateCustomer(CustomerRequestVO vo);
+
+    /**
+     * 删除客户(软删除)
+     */
+    Boolean deleteCustomer(Long id);
+
+    /**
+     * 导出客户数据(Excel)
+     *
+     * @param scope          导出范围:current / all / filtered
+     * @param fileName       文件名(不含扩展名)
+     * @param customerName   客户名称(模糊匹配,filtered 模式可用)
+     * @param lifecycleStatus 签约状态(filtered 模式可用)
+     * @param current        当前页码(current 模式必传)
+     * @param size           页大小(current 模式必传)
+     */
+    void exportCustomers(String scope, String fileName, String customerName,
+                         Integer lifecycleStatus, Integer current, Integer size,
+                         HttpServletResponse response);
+
+    // ==================== 联系人管理 ====================
+
+    /**
+     * 分页查询联系人
+     */
+    CommonPage<VppCustomerContact> listContact(Long customerId, Integer pageNum, Integer pageSize);
+
+    /**
+     * 新增联系人
+     */
     VppCustomerContact addContact(Long customerId, CustomerContactRequest request);
 
+    /**
+     * 更新联系人
+     */
     void updateContact(Long customerId, Long contactId, CustomerContactRequest request);
 
+    /**
+     * 删除联系人
+     */
     void deleteContact(Long customerId, Long contactId);
 }

+ 1051 - 81
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/impl/VppCustomerServiceImpl.java

@@ -1,40 +1,67 @@
 package com.usky.vpp.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.common.core.exception.BusinessException;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.vpp.domain.VppContract;
 import com.usky.vpp.domain.VppCustomer;
 import com.usky.vpp.domain.VppCustomerAccess;
 import com.usky.vpp.domain.VppCustomerContact;
+import com.usky.vpp.mapper.VppContractMapper;
 import com.usky.vpp.mapper.VppCustomerAccessMapper;
 import com.usky.vpp.mapper.VppCustomerContactMapper;
 import com.usky.vpp.mapper.VppCustomerMapper;
 import com.usky.vpp.service.VppCustomerService;
 import com.usky.vpp.service.vo.CustomerAccessAuditRequest;
-import com.usky.vpp.service.vo.CustomerAccessRequest;
+import com.usky.vpp.service.vo.CustomerAccessRequestVO;
+import com.usky.vpp.service.vo.CustomerAccessResponseVO;
 import com.usky.vpp.service.vo.CustomerContactRequest;
+import com.usky.vpp.service.vo.CustomerRequestVO;
+import com.usky.vpp.service.vo.CustomerResponseVO;
 import com.usky.vpp.util.VppAuditHelper;
-import com.usky.vpp.util.VppPageHelper;
+import com.usky.vpp.util.VppFieldValidator;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
-import org.springframework.util.StringUtils;
 
+import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
 /**
  * 客户管理业务实现
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
  */
+@Slf4j
 @Service
 public class VppCustomerServiceImpl implements VppCustomerService {
 
     private static final int ACCESS_PENDING = 0;
     private static final int ACCESS_PASSED = 1;
     private static final int ACCESS_REJECTED = 2;
-    private static final int LIFECYCLE_PENDING = 1;
-    private static final int LIFECYCLE_ADMITTED = 2;
+    private static final int LIFECYCLE_ADMITTED = 1;
 
     @Autowired
     private VppCustomerMapper customerMapper;
@@ -42,11 +69,26 @@ public class VppCustomerServiceImpl implements VppCustomerService {
     private VppCustomerAccessMapper accessMapper;
     @Autowired
     private VppCustomerContactMapper contactMapper;
+    @Autowired
+    private VppContractMapper contractMapper;
+
+    // ==================== 准入管理 ====================
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public VppCustomerAccess submitAccess(CustomerAccessRequest request) {
+    public Boolean submitAccess(CustomerAccessRequestVO request) {
         validateAccessRequest(request);
+
+        // 校验同租户下 accountNo 唯一
+        LambdaQueryWrapper<VppCustomerAccess> dupWrapper = new LambdaQueryWrapper<>();
+        dupWrapper.eq(VppCustomerAccess::getAccountNo, request.getAccountNo())
+                .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
+                .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+        Integer accessCount = accessMapper.selectCount(dupWrapper);
+        if (accessCount != null && accessCount > 0) {
+            throw new BusinessException("该电力户号已存在准入申请,请勿重复提交!");
+        }
+
         VppCustomerAccess access = new VppCustomerAccess();
         access.setApplyNo(VppAuditHelper.nextApplyNo());
         access.setAccountNo(request.getAccountNo());
@@ -58,112 +100,797 @@ public class VppCustomerServiceImpl implements VppCustomerService {
         access.setCreditStatus(request.getCreditStatus());
         access.setAccessStatus(ACCESS_PENDING);
         access.setApplyAt(LocalDateTime.now());
+        // 暂存联系人信息(审核通过时写入联系人表)
+        access.setContactName(request.getContactName());
+        access.setContactPhone(request.getContactPhone());
+        access.setContactEmail(request.getContactEmail());
+        access.setSigningDate(request.getSigningDate());
         VppAuditHelper.fillCreate(access);
-        accessMapper.insert(access);
-        return access;
+
+        int insert = accessMapper.insert(access);
+        return insert > 0;
     }
 
     @Override
-    public CommonPage<VppCustomerAccess> pageAccess(Map<String, Object> params) {
-        Page<VppCustomerAccess> page = VppPageHelper.of(params);
+    public CommonPage<CustomerAccessResponseVO> pageAccess(Long id, Integer accessStatus, String customerName,
+                                                           LocalDateTime startTime, LocalDateTime endTime,
+                                                           Integer pageNum, Integer pageSize) {
+        IPage<VppCustomerAccess> page = new Page<>(pageNum, pageSize);
+
         LambdaQueryWrapper<VppCustomerAccess> wrapper = new LambdaQueryWrapper<VppCustomerAccess>()
+                .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
                 .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(id != null, VppCustomerAccess::getId, id)
+                .eq(accessStatus != null, VppCustomerAccess::getAccessStatus, accessStatus)
+                .like(StringUtils.isNotBlank(customerName), VppCustomerAccess::getCustomerName, customerName)
+                .ge(startTime != null, VppCustomerAccess::getApplyAt, startTime)
+                .le(endTime != null, VppCustomerAccess::getApplyAt, endTime)
                 .orderByDesc(VppCustomerAccess::getApplyAt);
-        if (params != null && params.get("accessStatus") != null) {
-            wrapper.eq(VppCustomerAccess::getAccessStatus, Integer.parseInt(params.get("accessStatus").toString()));
+
+        page = accessMapper.selectPage(page, wrapper);
+
+        List<VppCustomerAccess> records = page.getRecords();
+        if (records == null || records.isEmpty()) {
+            return new CommonPage<>(Collections.emptyList(), page.getTotal(), pageSize, pageNum);
         }
-        Page<VppCustomerAccess> result = accessMapper.selectPage(page, wrapper);
-        return toCommonPage(result);
+
+        List<CustomerAccessResponseVO> list = records.stream().map(entity -> {
+            CustomerAccessResponseVO vo = new CustomerAccessResponseVO();
+            BeanUtils.copyProperties(entity, vo);
+            return vo;
+        }).collect(Collectors.toList());
+
+        return new CommonPage<>(list, page.getTotal(), pageSize, pageNum);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void auditAccess(Long id, CustomerAccessAuditRequest request) {
-        VppCustomerAccess access = accessMapper.selectById(id);
-        if (access == null || VppAuditHelper.isDeleted(access.getDeleteFlag())) {
-            throw new BusinessException("准入申请不存在");
+    public void auditAccess(CustomerAccessAuditRequest request) {
+        if (request.getId() == null) {
+            throw new BusinessException("准入申请ID不能为空!");
         }
-        if (access.getAccessStatus() != ACCESS_PENDING) {
-            throw new BusinessException("当前状态不允许审核");
+        if (request.getResult() == null) {
+            throw new BusinessException("审核结果不能为空!");
         }
-        if (request.getPassed() == null) {
-            throw new BusinessException("审核结果不能为空");
+        Long id = request.getId();
+        VppCustomerAccess access = findAccessById(id);
+
+        if (access.getAccessStatus() != ACCESS_PENDING) {
+            throw new BusinessException("当前状态不允许审核,仅待审核状态可操作!");
         }
-        access.setAuditOpinion(request.getAuditOpinion());
+
+        access.setAuditOpinion(request.getComment());
         access.setAuditAt(LocalDateTime.now());
-        if (Boolean.TRUE.equals(request.getPassed())) {
+
+        if (request.getResult() == ACCESS_PASSED) {
+            // 审核通过:校验同租户下 accountNo 和 customerName 唯一
+            Integer tenantId = SecurityUtils.getTenantId();
+            LambdaQueryWrapper<VppCustomer> dupWrapper = new LambdaQueryWrapper<>();
+            dupWrapper.eq(VppCustomer::getTenantId, tenantId)
+                    .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                    .and(w -> w.eq(VppCustomer::getAccountNo, access.getAccountNo())
+                            .or().eq(VppCustomer::getCustomerName, access.getCustomerName()));
+            Integer dupCount = customerMapper.selectCount(dupWrapper);
+            if (dupCount != null && dupCount > 0) {
+                throw new BusinessException("同租户下已存在相同电力户号或客户名称的客户!");
+            }
+
             access.setAccessStatus(ACCESS_PASSED);
             VppCustomer customer = buildCustomerFromAccess(access);
             VppAuditHelper.fillCreate(customer);
             customerMapper.insert(customer);
             access.setCustomerId(customer.getId());
+
+            // 审核通过时,同步创建主联系人
+            if (StringUtils.isNotBlank(access.getContactName())) {
+                VppCustomerContact contact = new VppCustomerContact();
+                contact.setCustomerId(customer.getId());
+                contact.setContactName(access.getContactName());
+                contact.setContactPhone(access.getContactPhone());
+                contact.setContactEmail(access.getContactEmail());
+                contact.setIsPrimary(1);
+                VppAuditHelper.fillCreate(contact);
+                contactMapper.insert(contact);
+            }
         } else {
             access.setAccessStatus(ACCESS_REJECTED);
         }
+
         VppAuditHelper.fillUpdate(access);
         accessMapper.updateById(access);
     }
 
     @Override
-    public CommonPage<VppCustomer> pageCustomer(Map<String, Object> params) {
-        Page<VppCustomer> page = VppPageHelper.of(params);
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteAccess(Long id) {
+        VppCustomerAccess access = findAccessById(id);
+        if (access.getAccessStatus() == ACCESS_PENDING) {
+            throw new BusinessException("待审核的准入申请无法删除,请先审核或驳回后再操作!");
+        }
+        VppAuditHelper.fillSoftDelete(access);
+        int result = accessMapper.updateById(access);
+        return result > 0;
+    }
+
+    @Override
+    public void exportAccess(String scope, String fileName, Long id, Integer accessStatus,
+                             String customerName, LocalDateTime startTime, LocalDateTime endTime,
+                             Integer current, Integer size, HttpServletResponse response) {
+        List<VppCustomerAccess> accessList;
+
+        switch (scope) {
+            case "current":
+                if (current == null || size == null) {
+                    throw new BusinessException("current 模式下分页参数 current 和 size 不能为空!");
+                }
+                IPage<VppCustomerAccess> page = new Page<>(current, size);
+                LambdaQueryWrapper<VppCustomerAccess> currWrapper = new LambdaQueryWrapper<VppCustomerAccess>()
+                        .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .orderByDesc(VppCustomerAccess::getApplyAt);
+                page = accessMapper.selectPage(page, currWrapper);
+                accessList = page.getRecords();
+                break;
+
+            case "all":
+                LambdaQueryWrapper<VppCustomerAccess> allWrapper = new LambdaQueryWrapper<VppCustomerAccess>()
+                        .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .orderByDesc(VppCustomerAccess::getApplyAt);
+                accessList = accessMapper.selectList(allWrapper);
+                break;
+
+            case "filtered":
+                boolean hasFilter = id != null || accessStatus != null
+                        || StringUtils.isNotBlank(customerName)
+                        || startTime != null || endTime != null;
+                if (!hasFilter) {
+                    throw new BusinessException("filtered 模式下请至少提供一个筛选条件(id / accessStatus / customerName / startTime / endTime)!");
+                }
+                LambdaQueryWrapper<VppCustomerAccess> filteredWrapper = new LambdaQueryWrapper<VppCustomerAccess>()
+                        .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomerAccess::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .eq(id != null, VppCustomerAccess::getId, id)
+                        .eq(accessStatus != null, VppCustomerAccess::getAccessStatus, accessStatus)
+                        .like(StringUtils.isNotBlank(customerName), VppCustomerAccess::getCustomerName, customerName)
+                        .ge(startTime != null, VppCustomerAccess::getApplyAt, startTime)
+                        .le(endTime != null, VppCustomerAccess::getApplyAt, endTime)
+                        .orderByDesc(VppCustomerAccess::getApplyAt);
+                accessList = accessMapper.selectList(filteredWrapper);
+                break;
+
+            default:
+                throw new BusinessException("不支持的导出范围:" + scope + ",请使用 current / all / filtered");
+        }
+
+        if (accessList == null) {
+            accessList = Collections.emptyList();
+        }
+
+        // 生成 Excel
+        try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+            org.apache.poi.xssf.usermodel.XSSFSheet sheet = workbook.createSheet("准入申请数据");
+
+            // 表头样式
+            org.apache.poi.xssf.usermodel.XSSFCellStyle headerStyle = workbook.createCellStyle();
+            headerStyle.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.GREY_25_PERCENT.getIndex());
+            headerStyle.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
+            org.apache.poi.xssf.usermodel.XSSFFont headerFont = workbook.createFont();
+            headerFont.setBold(true);
+            headerStyle.setFont(headerFont);
+
+            // 表头
+            String[] headers = {
+                    "序号", "申请编号", "电力户号", "客户名称", "客户类型",
+                    "供电公司", "用电容量(kW)", "信用状态", "审核状态",
+                    "审核意见", "营业执照",
+                    //"签约日期", "联系人姓名", "联系人电话", "联系人邮箱",
+                    "申请时间", "审核时间", "创建时间"
+            };
+            org.apache.poi.xssf.usermodel.XSSFRow headerRow = sheet.createRow(0);
+            for (int i = 0; i < headers.length; i++) {
+                org.apache.poi.xssf.usermodel.XSSFCell cell = headerRow.createCell(i);
+                cell.setCellValue(headers[i]);
+                cell.setCellStyle(headerStyle);
+            }
+
+            // 数据行
+            int rowIdx = 1;
+            for (VppCustomerAccess acc : accessList) {
+                org.apache.poi.xssf.usermodel.XSSFRow row = sheet.createRow(rowIdx++);
+                setCell(row, 0, rowIdx - 1);
+                setCell(row, 1, acc.getApplyNo());
+                setCell(row, 2, acc.getAccountNo());
+                setCell(row, 3, acc.getCustomerName());
+                setCell(row, 4, typeLabel(acc.getCustomerType(), "中高压", "低压商用", "居民充电桩", "自有资产"));
+                setCell(row, 5, acc.getPowerCompany());
+                setCell(row, 6, numberStr(acc.getContractCapacity()));
+                setCell(row, 7, typeLabel(acc.getCreditStatus(), "正常", "关注", "不良"));
+                setCell(row, 8, accessStatusLabel(acc.getAccessStatus()));
+                setCell(row, 9, acc.getAuditOpinion());
+                insertLicenseImage(workbook, sheet, row, 10, acc.getBusinessLicenseUrl());
+                // setCell(row, 11, dateStr(acc.getSigningDate()));
+                // setCell(row, 12, acc.getContactName());
+                // setCell(row, 13, acc.getContactPhone());
+                // setCell(row, 14, acc.getContactEmail());
+                setCell(row, 11, dateTimeStr(acc.getApplyAt()));
+                setCell(row, 12, dateTimeStr(acc.getAuditAt()));
+                setCell(row, 13, dateTimeStr(acc.getCreateTime()));
+            }
+
+            // 营业执照列宽度
+            sheet.setColumnWidth(10, 28 * 256);
+
+            // 自动调整列宽
+            for (int i = 0; i < headers.length; i++) {
+                if (i == 10) {
+                    continue;
+                }
+                sheet.autoSizeColumn(i);
+                int width = sheet.getColumnWidth(i);
+                if (width > 15000) {
+                    sheet.setColumnWidth(i, 15000);
+                }
+            }
+
+            // 写入响应
+            String encodedFileName = java.net.URLEncoder.encode(fileName + ".xlsx", StandardCharsets.UTF_8.name())
+                    .replace("+", "%20");
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+            response.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName
+                    + "; filename*=UTF-8''" + encodedFileName);
+            workbook.write(response.getOutputStream());
+            response.getOutputStream().flush();
+
+        } catch (IOException e) {
+            throw new BusinessException("导出准入申请数据失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 客户管理 ====================
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean createCustomer(CustomerRequestVO vo) {
+        // 必填校验
+        if (StringUtils.isBlank(vo.getAccountNo())) {
+            throw new BusinessException("电力户号不能为空!");
+        }
+        if (StringUtils.isBlank(vo.getCustomerName())) {
+            throw new BusinessException("客户名称不能为空!");
+        }
+        if (vo.getCustomerType() == null) {
+            throw new BusinessException("客户类型不能为空!");
+        }
+        if (StringUtils.isBlank(vo.getPowerCompany())) {
+            throw new BusinessException("供电公司不能为空!");
+        }
+        if (vo.getContractCapacity() == null) {
+            throw new BusinessException("用电容量不能为空!");
+        }
+
+        // 同租户下 accountNo 和 customerName 唯一校验
+        Integer tenantId = SecurityUtils.getTenantId();
+        LambdaQueryWrapper<VppCustomer> dupWrapper = new LambdaQueryWrapper<>();
+        dupWrapper.eq(VppCustomer::getTenantId, tenantId)
+                .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .and(w -> w.eq(VppCustomer::getAccountNo, vo.getAccountNo())
+                        .or().eq(VppCustomer::getCustomerName, vo.getCustomerName()));
+        Integer dupCount = customerMapper.selectCount(dupWrapper);
+        if (dupCount != null && dupCount > 0) {
+            throw new BusinessException("同租户下已存在相同电力户号或客户名称的客户!");
+        }
+
+        // 构建客户
+        VppCustomer customer = new VppCustomer();
+        customer.setAccountNo(vo.getAccountNo());
+        customer.setCustomerName(vo.getCustomerName());
+        customer.setCustomerType(vo.getCustomerType());
+        customer.setPowerCompany(vo.getPowerCompany());
+        customer.setContractCapacity(vo.getContractCapacity());
+        customer.setCreditStatus(vo.getCreditStatus());
+        customer.setBusinessLicenseUrl(vo.getBusinessLicenseUrl());
+        customer.setLifecycleStatus(vo.getLifecycleStatus());
+        customer.setDrNotifyMinutes(vo.getDrNotifyMinutes());
+        customer.setDrUpCapacityKw(vo.getDrUpCapacityKw());
+        customer.setDrDownCapacityKw(vo.getDrDownCapacityKw());
+        customer.setProvince(vo.getProvince());
+        customer.setCity(vo.getCity());
+        customer.setDistrict(vo.getDistrict());
+        customer.setAddress(vo.getAddress());
+        customer.setRemark(vo.getRemark());
+        customer.setIsVppResource(vo.getIsVppResource());
+        customer.setVppCategory(vo.getVppCategory());
+        customer.setDrResourceType(vo.getDrResourceType());
+        customer.setVppProxyCode(vo.getVppProxyCode());
+        customer.setIndustry(vo.getIndustry());
+        customer.setIndustrySector(vo.getIndustrySector());
+        customer.setRunningCapacity(vo.getRunningCapacity());
+        customer.setProductionStartDate(vo.getProductionStartDate());
+        customer.setProductionEndDate(vo.getProductionEndDate());
+        customer.setPowerAddress(vo.getPowerAddress());
+        customer.setSigningDate(vo.getSigningDate());
+        VppAuditHelper.fillCreate(customer);
+        customerMapper.insert(customer);
+
+        // 若有联系人信息,创建主联系人
+        if (StringUtils.isNotBlank(vo.getContactName())) {
+            VppFieldValidator.validatePhone(vo.getContactPhone());
+            if (StringUtils.isNotBlank(vo.getContactEmail())) {
+                VppFieldValidator.validateEmail(vo.getContactEmail());
+            }
+            VppCustomerContact contact = new VppCustomerContact();
+            contact.setCustomerId(customer.getId());
+            contact.setContactName(vo.getContactName());
+            contact.setContactPhone(vo.getContactPhone());
+            contact.setContactEmail(vo.getContactEmail());
+            contact.setIsPrimary(1);
+            VppAuditHelper.fillCreate(contact);
+            contactMapper.insert(contact);
+        }
+
+        return true;
+    }
+
+    @Override
+    public CommonPage<CustomerResponseVO> pageCustomer(Long id, String customerName, String accountNo,
+                                                       Integer lifecycleStatus, Integer customerType,
+                                                       Integer pageNum, Integer pageSize) {
+        IPage<VppCustomer> page = new Page<>(pageNum, pageSize);
+
         LambdaQueryWrapper<VppCustomer> wrapper = new LambdaQueryWrapper<VppCustomer>()
+                .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
                 .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(id != null, VppCustomer::getId, id)
+                .like(StringUtils.isNotBlank(customerName), VppCustomer::getCustomerName, customerName)
+                .eq(StringUtils.isNotBlank(accountNo), VppCustomer::getAccountNo, accountNo)
+                .eq(lifecycleStatus != null, VppCustomer::getLifecycleStatus, lifecycleStatus)
+                .eq(customerType != null, VppCustomer::getCustomerType, customerType)
                 .orderByDesc(VppCustomer::getCreateTime);
-        if (params != null) {
-            if (params.get("customerName") != null) {
-                wrapper.like(VppCustomer::getCustomerName, params.get("customerName").toString());
+
+        page = customerMapper.selectPage(page, wrapper);
+        List<VppCustomer> records = page.getRecords();
+
+        if (records == null || records.isEmpty()) {
+            return new CommonPage<>(Collections.emptyList(), page.getTotal(), pageSize, pageNum);
+        }
+
+        // 批量查询关联合同
+        final Set<Long> customerIds = records.stream()
+                .map(VppCustomer::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        final Map<Long, List<VppContract>> contractMap;
+        if (!customerIds.isEmpty()) {
+            LambdaQueryWrapper<VppContract> contractWrapper = new LambdaQueryWrapper<>();
+            contractWrapper.in(VppContract::getCustomerId, customerIds)
+                    .eq(VppContract::getDeleteFlag, 0)
+                    .orderByDesc(VppContract::getCreateTime);
+            List<VppContract> allContracts = contractMapper.selectList(contractWrapper);
+            contractMap = allContracts.stream()
+                    .collect(Collectors.groupingBy(VppContract::getCustomerId));
+        } else {
+            contractMap = Collections.emptyMap();
+        }
+
+        // 批量查询关联联系人(取主联系人 is_primary=1)
+        final Map<Long, VppCustomerContact> contactMap;
+        if (!customerIds.isEmpty()) {
+            LambdaQueryWrapper<VppCustomerContact> contactWrapper = new LambdaQueryWrapper<>();
+            contactWrapper.in(VppCustomerContact::getCustomerId, customerIds)
+                    .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
+                    .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                    .eq(VppCustomerContact::getIsPrimary, 1);
+            List<VppCustomerContact> allContacts = contactMapper.selectList(contactWrapper);
+            contactMap = allContacts.stream()
+                    .collect(Collectors.toMap(
+                            VppCustomerContact::getCustomerId,
+                            Function.identity(),
+                            (a, b) -> a));
+        } else {
+            contactMap = Collections.emptyMap();
+        }
+
+        // 组装响应VO
+        List<CustomerResponseVO> list = records.stream().map(entity -> {
+            CustomerResponseVO vo = new CustomerResponseVO();
+            BeanUtils.copyProperties(entity, vo);
+            // 设置关联合同
+            List<VppContract> contracts = contractMap.getOrDefault(entity.getId(), Collections.emptyList());
+            vo.setContracts(contracts.stream().map(this::toContractBrief).collect(Collectors.toList()));
+            // 设置关联联系人
+            VppCustomerContact contact = contactMap.get(entity.getId());
+            if (contact != null) {
+                vo.setContactName(contact.getContactName());
+                vo.setContactPhone(contact.getContactPhone());
+                vo.setContactEmail(contact.getContactEmail());
             }
-            if (params.get("accountNo") != null) {
-                wrapper.eq(VppCustomer::getAccountNo, params.get("accountNo").toString());
+            return vo;
+        }).collect(Collectors.toList());
+
+        return new CommonPage<>(list, page.getTotal(), pageSize, pageNum);
+    }
+
+    @Override
+    public CustomerResponseVO getCustomer(Long id) {
+        VppCustomer customer = findCustomerById(id);
+
+        CustomerResponseVO vo = new CustomerResponseVO();
+        BeanUtils.copyProperties(customer, vo);
+
+        // 关联查询合同
+        LambdaQueryWrapper<VppContract> contractWrapper = new LambdaQueryWrapper<>();
+        contractWrapper.eq(VppContract::getCustomerId, id)
+                .eq(VppContract::getDeleteFlag, 0)
+                .orderByDesc(VppContract::getCreateTime);
+        List<VppContract> contracts = contractMapper.selectList(contractWrapper);
+        vo.setContracts(contracts.stream().map(this::toContractBrief).collect(Collectors.toList()));
+
+        // 关联查询主联系人
+        LambdaQueryWrapper<VppCustomerContact> contactWrapper = new LambdaQueryWrapper<>();
+        contactWrapper.eq(VppCustomerContact::getCustomerId, id)
+                .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
+                .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                .eq(VppCustomerContact::getIsPrimary, 1);
+        VppCustomerContact contact = contactMapper.selectOne(contactWrapper);
+        if (contact != null) {
+            vo.setContactName(contact.getContactName());
+            vo.setContactPhone(contact.getContactPhone());
+            vo.setContactEmail(contact.getContactEmail());
+        }
+
+        return vo;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean updateCustomer(CustomerRequestVO vo) {
+        if (vo.getId() == null) {
+            throw new BusinessException("客户ID不能为空!");
+        }
+        Long id = vo.getId();
+        VppCustomer existing = findCustomerById(id);
+
+        // 仅更新非空字段
+        if (StringUtils.isNotBlank(vo.getCustomerName())) {
+            if (!vo.getCustomerName().equals(existing.getCustomerName())) {
+                // 校验同租户下客户名称唯一
+                LambdaQueryWrapper<VppCustomer> uniqueWrapper = new LambdaQueryWrapper<>();
+                uniqueWrapper.eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomer::getCustomerName, vo.getCustomerName())
+                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED);
+                Integer count = customerMapper.selectCount(uniqueWrapper);
+                if (count != null && count > 0) {
+                    throw new BusinessException("客户名称已存在,请更换!");
+                }
             }
-            if (params.get("lifecycleStatus") != null) {
-                wrapper.eq(VppCustomer::getLifecycleStatus, Integer.parseInt(params.get("lifecycleStatus").toString()));
+            existing.setCustomerName(vo.getCustomerName());
+        }
+        if (vo.getCustomerType() != null) {
+            existing.setCustomerType(vo.getCustomerType());
+        }
+        if (StringUtils.isNotBlank(vo.getPowerCompany())) {
+            existing.setPowerCompany(vo.getPowerCompany());
+        }
+        if (vo.getContractCapacity() != null) {
+            existing.setContractCapacity(vo.getContractCapacity());
+        }
+        if (vo.getCreditStatus() != null) {
+            existing.setCreditStatus(vo.getCreditStatus());
+        }
+        if (vo.getLifecycleStatus() != null) {
+            existing.setLifecycleStatus(vo.getLifecycleStatus());
+        }
+        if (vo.getDrNotifyMinutes() != null) {
+            existing.setDrNotifyMinutes(vo.getDrNotifyMinutes());
+        }
+        if (vo.getDrUpCapacityKw() != null) {
+            existing.setDrUpCapacityKw(vo.getDrUpCapacityKw());
+        }
+        if (vo.getDrDownCapacityKw() != null) {
+            existing.setDrDownCapacityKw(vo.getDrDownCapacityKw());
+        }
+        if (vo.getProvince() != null) {
+            existing.setProvince(vo.getProvince());
+        }
+        if (vo.getCity() != null) {
+            existing.setCity(vo.getCity());
+        }
+        if (vo.getDistrict() != null) {
+            existing.setDistrict(vo.getDistrict());
+        }
+        if (vo.getAddress() != null) {
+            existing.setAddress(vo.getAddress());
+        }
+        if (vo.getBusinessLicenseUrl() != null) {
+            existing.setBusinessLicenseUrl(vo.getBusinessLicenseUrl());
+        }
+        if (vo.getRemark() != null) {
+            existing.setRemark(vo.getRemark());
+        }
+        if (vo.getIsVppResource() != null) {
+            existing.setIsVppResource(vo.getIsVppResource());
+        }
+        if (vo.getVppCategory() != null) {
+            existing.setVppCategory(vo.getVppCategory());
+        }
+        if (vo.getDrResourceType() != null) {
+            existing.setDrResourceType(vo.getDrResourceType());
+        }
+        if (vo.getVppProxyCode() != null) {
+            existing.setVppProxyCode(vo.getVppProxyCode());
+        }
+        if (vo.getIndustry() != null) {
+            existing.setIndustry(vo.getIndustry());
+        }
+        if (vo.getIndustrySector() != null) {
+            existing.setIndustrySector(vo.getIndustrySector());
+        }
+        if (vo.getRunningCapacity() != null) {
+            existing.setRunningCapacity(vo.getRunningCapacity());
+        }
+        if (vo.getProductionStartDate() != null) {
+            existing.setProductionStartDate(vo.getProductionStartDate());
+        }
+        if (vo.getProductionEndDate() != null) {
+            existing.setProductionEndDate(vo.getProductionEndDate());
+        }
+        if (vo.getPowerAddress() != null) {
+            existing.setPowerAddress(vo.getPowerAddress());
+        }
+        if (vo.getSigningDate() != null) {
+            existing.setSigningDate(vo.getSigningDate());
+        }
+
+        // 同步维护主联系人:如果传了联系人姓名,则 upsert 主联系人
+        if (StringUtils.isNotBlank(vo.getContactName())) {
+            // 校验联系人信息
+            VppFieldValidator.validatePhone(vo.getContactPhone());
+            if (StringUtils.isNotBlank(vo.getContactEmail())) {
+                VppFieldValidator.validateEmail(vo.getContactEmail());
             }
-            if (params.get("customerType") != null) {
-                wrapper.eq(VppCustomer::getCustomerType, Integer.parseInt(params.get("customerType").toString()));
+
+            LambdaQueryWrapper<VppCustomerContact> contactWrapper = new LambdaQueryWrapper<>();
+            contactWrapper.eq(VppCustomerContact::getCustomerId, id)
+                    .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
+                    .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                    .eq(VppCustomerContact::getIsPrimary, 1);
+            VppCustomerContact contact = contactMapper.selectOne(contactWrapper);
+
+            if (contact != null) {
+                // 更新已有主联系人
+                contact.setContactName(vo.getContactName());
+                contact.setContactPhone(vo.getContactPhone());
+                contact.setContactEmail(vo.getContactEmail());
+                VppAuditHelper.fillUpdate(contact);
+                contactMapper.updateById(contact);
+            } else {
+                // 新增主联系人
+                contact = new VppCustomerContact();
+                contact.setCustomerId(id);
+                contact.setContactName(vo.getContactName());
+                contact.setContactPhone(vo.getContactPhone());
+                contact.setContactEmail(vo.getContactEmail());
+                contact.setIsPrimary(1);
+                VppAuditHelper.fillCreate(contact);
+                contactMapper.insert(contact);
             }
         }
-        return toCommonPage(customerMapper.selectPage(page, wrapper));
+
+        VppAuditHelper.fillUpdate(existing);
+        int result = customerMapper.updateById(existing);
+        return result > 0;
     }
 
     @Override
-    public VppCustomer getCustomer(Long id) {
-        VppCustomer customer = customerMapper.selectById(id);
-        if (customer == null || VppAuditHelper.isDeleted(customer.getDeleteFlag())) {
-            throw new BusinessException("客户不存在");
-        }
-        return customer;
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteCustomer(Long id) {
+        VppCustomer customer = findCustomerById(id);
+
+        VppAuditHelper.fillSoftDelete(customer);
+        int result = customerMapper.updateById(customer);
+        return result > 0;
     }
 
     @Override
-    @Transactional(rollbackFor = Exception.class)
-    public void updateCustomer(Long id, VppCustomer customer) {
-        VppCustomer existing = getCustomer(id);
-        customer.setId(existing.getId());
-        customer.setAccountNo(existing.getAccountNo());
-        customer.setCreateTime(existing.getCreateTime());
-        customer.setCreatedBy(existing.getCreatedBy());
-        VppAuditHelper.fillUpdate(customer);
-        customerMapper.updateById(customer);
+    public void exportCustomers(String scope, String fileName, String customerName,
+                                Integer lifecycleStatus, Integer current, Integer size,
+                                HttpServletResponse response) {
+        List<VppCustomer> customers;
+
+        switch (scope) {
+            case "current":
+                // current 模式:必须传分页参数
+                if (current == null || size == null) {
+                    throw new BusinessException("current 模式下分页参数 current 和 size 不能为空!");
+                }
+                IPage<VppCustomer> page = new Page<>(current, size);
+                LambdaQueryWrapper<VppCustomer> currWrapper = new LambdaQueryWrapper<VppCustomer>()
+                        .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .orderByDesc(VppCustomer::getCreateTime);
+                page = customerMapper.selectPage(page, currWrapper);
+                customers = page.getRecords();
+                break;
+
+            case "all":
+                // all 模式:当前租户下所有未删除客户
+                LambdaQueryWrapper<VppCustomer> allWrapper = new LambdaQueryWrapper<VppCustomer>()
+                        .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .orderByDesc(VppCustomer::getCreateTime);
+                customers = customerMapper.selectList(allWrapper);
+                break;
+
+            case "filtered":
+                // filtered 模式:必须至少传一个筛选条件
+                boolean hasFilter = StringUtils.isNotBlank(customerName) || lifecycleStatus != null;
+                if (!hasFilter) {
+                    throw new BusinessException("filtered 模式下请至少提供一个筛选条件(customerName / lifecycleStatus)!");
+                }
+                LambdaQueryWrapper<VppCustomer> filteredWrapper = new LambdaQueryWrapper<VppCustomer>()
+                        .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId())
+                        .eq(VppCustomer::getDeleteFlag, VppAuditHelper.NOT_DELETED)
+                        .like(StringUtils.isNotBlank(customerName), VppCustomer::getCustomerName, customerName)
+                        .eq(lifecycleStatus != null, VppCustomer::getLifecycleStatus, lifecycleStatus)
+                        .orderByDesc(VppCustomer::getCreateTime);
+                customers = customerMapper.selectList(filteredWrapper);
+                break;
+
+            default:
+                throw new BusinessException("不支持的导出范围:" + scope + ",请使用 current / all / filtered");
+        }
+
+        if (customers == null) {
+            customers = Collections.emptyList();
+        }
+
+        // 批量查询关联合同
+        final Set<Long> customerIds = customers.stream()
+                .map(VppCustomer::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        final Map<Long, List<VppContract>> contractMap;
+        if (!customerIds.isEmpty()) {
+            LambdaQueryWrapper<VppContract> contractWrapper = new LambdaQueryWrapper<>();
+            contractWrapper.in(VppContract::getCustomerId, customerIds)
+                    .eq(VppContract::getDeleteFlag, 0);
+            List<VppContract> allContracts = contractMapper.selectList(contractWrapper);
+            contractMap = allContracts.stream()
+                    .collect(Collectors.groupingBy(VppContract::getCustomerId));
+        } else {
+            contractMap = Collections.emptyMap();
+        }
+
+        // 生成 Excel
+        try (org.apache.poi.xssf.usermodel.XSSFWorkbook workbook = new org.apache.poi.xssf.usermodel.XSSFWorkbook()) {
+            org.apache.poi.xssf.usermodel.XSSFSheet sheet = workbook.createSheet("客户数据");
+
+            // 创建表头样式
+            org.apache.poi.xssf.usermodel.XSSFCellStyle headerStyle = workbook.createCellStyle();
+            headerStyle.setFillForegroundColor(org.apache.poi.ss.usermodel.IndexedColors.GREY_25_PERCENT.getIndex());
+            headerStyle.setFillPattern(org.apache.poi.ss.usermodel.FillPatternType.SOLID_FOREGROUND);
+            org.apache.poi.xssf.usermodel.XSSFFont headerFont = workbook.createFont();
+            headerFont.setBold(true);
+            headerStyle.setFont(headerFont);
+
+            // 表头
+            String[] headers = {
+                    "序号", "电力户号", "客户名称", "客户类型", "供电公司",
+                    "签约容量(kW)", "信用状态", "签约状态",
+                    "省", "市", "区/县", "地址", "用电地址",
+                    "是否VPP资源", "VPP分类", "需求响应资源分类",
+                    "VPP代理编码", "所属行业", "所属产业", "运行容量(kW)",
+                    "生产经营开始", "生产经营结束", "合同编号列表",
+                    "营业执照", "签约日期", "备注", "创建时间"
+            };
+            org.apache.poi.xssf.usermodel.XSSFRow headerRow = sheet.createRow(0);
+            for (int i = 0; i < headers.length; i++) {
+                org.apache.poi.xssf.usermodel.XSSFCell cell = headerRow.createCell(i);
+                cell.setCellValue(headers[i]);
+                cell.setCellStyle(headerStyle);
+            }
+
+            // 数据行
+            int rowIdx = 1;
+            for (VppCustomer c : customers) {
+                org.apache.poi.xssf.usermodel.XSSFRow row = sheet.createRow(rowIdx++);
+                List<VppContract> contracts = contractMap.getOrDefault(c.getId(), Collections.emptyList());
+                String contractNos = contracts.stream()
+                        .map(VppContract::getContractNo)
+                        .collect(Collectors.joining("; "));
+
+                setCell(row, 0, rowIdx - 1);                       // 序号
+                setCell(row, 1, c.getAccountNo());
+                setCell(row, 2, c.getCustomerName());
+                setCell(row, 3, typeLabel(c.getCustomerType(), "中高压", "低压商用", "居民充电桩", "自有资产"));
+                setCell(row, 4, c.getPowerCompany());
+                setCell(row, 5, numberStr(c.getContractCapacity()));
+                setCell(row, 6, typeLabel(c.getCreditStatus(), "正常", "关注", "不良"));
+                setCell(row, 7, typeLabel(c.getLifecycleStatus(), "待签约", "已签约", "履约中", "待续约", "已续约", "已解约"));
+                setCell(row, 8, c.getProvince());
+                setCell(row, 9, c.getCity());
+                setCell(row, 10, c.getDistrict());
+                setCell(row, 11, c.getAddress());
+                setCell(row, 12, c.getPowerAddress());
+                setCell(row, 13, c.getIsVppResource() != null && c.getIsVppResource() == 1 ? "是" : "否");
+                setCell(row, 14, c.getVppCategory());
+                setCell(row, 15, c.getDrResourceType());
+                setCell(row, 16, c.getVppProxyCode());
+                setCell(row, 17, c.getIndustry());
+                setCell(row, 18, c.getIndustrySector());
+                setCell(row, 19, numberStr(c.getRunningCapacity()));
+                setCell(row, 20, timeStr(c.getProductionStartDate()));
+                setCell(row, 21, timeStr(c.getProductionEndDate()));
+                setCell(row, 22, contractNos);
+                insertLicenseImage(workbook, sheet, row, 23, c.getBusinessLicenseUrl());
+                setCell(row, 24, dateStr(c.getSigningDate()));
+                setCell(row, 25, c.getRemark());
+                setCell(row, 26, dateTimeStr(c.getCreateTime()));
+            }
+
+            // 设置营业执照列宽度(固定宽度以适配图片)
+            sheet.setColumnWidth(23, 28 * 256); // 约 28 字符宽
+
+            // 自动调整列宽
+            for (int i = 0; i < headers.length; i++) {
+                if (i == 23) {
+                    continue; // 营业执照列已手动设置宽度
+                }
+                sheet.autoSizeColumn(i);
+                // 限制最大宽度,避免营业执照URL列过宽
+                int width = sheet.getColumnWidth(i);
+                if (width > 15000) {
+                    sheet.setColumnWidth(i, 15000);
+                }
+            }
+
+            // 写入响应
+            String encodedFileName = java.net.URLEncoder.encode(fileName + ".xlsx", StandardCharsets.UTF_8.name())
+                    .replace("+", "%20");
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+            response.setHeader("Content-Disposition", "attachment; filename=" + encodedFileName
+                    + "; filename*=UTF-8''" + encodedFileName);
+            workbook.write(response.getOutputStream());
+            response.getOutputStream().flush();
+
+        } catch (IOException e) {
+            throw new BusinessException("导出客户数据失败: " + e.getMessage());
+        }
     }
 
+    // ==================== 联系人管理 ====================
+
     @Override
-    public CommonPage<VppCustomerContact> listContact(Long customerId, Map<String, Object> params) {
-        getCustomer(customerId);
-        Page<VppCustomerContact> page = VppPageHelper.of(params);
+    public CommonPage<VppCustomerContact> listContact(Long customerId, Integer pageNum, Integer pageSize) {
+        // 校验客户是否存在(租户内)
+        findCustomerById(customerId);
+
+        IPage<VppCustomerContact> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<VppCustomerContact> wrapper = new LambdaQueryWrapper<VppCustomerContact>()
                 .eq(VppCustomerContact::getCustomerId, customerId)
+                .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId())
                 .eq(VppCustomerContact::getDeleteFlag, VppAuditHelper.NOT_DELETED)
-                .orderByDesc(VppCustomerContact::getIsPrimary);
-        return toCommonPage(contactMapper.selectPage(page, wrapper));
+                .orderByDesc(VppCustomerContact::getIsPrimary)
+                .orderByAsc(VppCustomerContact::getId);
+
+        page = contactMapper.selectPage(page, wrapper);
+
+        return new CommonPage<>(page.getRecords(), page.getTotal(), pageSize, pageNum);
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
     public VppCustomerContact addContact(Long customerId, CustomerContactRequest request) {
-        getCustomer(customerId);
+        // 校验客户是否存在(租户内)
+        findCustomerById(customerId);
+
         validateContactRequest(request);
+
         VppCustomerContact contact = new VppCustomerContact();
         contact.setCustomerId(customerId);
         contact.setContactName(request.getContactName());
@@ -181,6 +908,7 @@ public class VppCustomerServiceImpl implements VppCustomerService {
     public void updateContact(Long customerId, Long contactId, CustomerContactRequest request) {
         VppCustomerContact contact = getContact(customerId, contactId);
         validateContactRequest(request);
+
         contact.setContactName(request.getContactName());
         contact.setContactPhone(request.getContactPhone());
         contact.setContactEmail(request.getContactEmail());
@@ -188,6 +916,7 @@ public class VppCustomerServiceImpl implements VppCustomerService {
             contact.setIsPrimary(request.getIsPrimary());
         }
         contact.setPosition(request.getPosition());
+
         VppAuditHelper.fillUpdate(contact);
         contactMapper.updateById(contact);
     }
@@ -200,17 +929,7 @@ public class VppCustomerServiceImpl implements VppCustomerService {
         contactMapper.updateById(contact);
     }
 
-    private <T> CommonPage<T> toCommonPage(Page<T> page) {
-        return new CommonPage<>(page.getRecords(), page.getTotal(), page.getCurrent(), page.getSize());
-    }
-
-    private VppCustomerContact getContact(Long customerId, Long contactId) {
-        VppCustomerContact contact = contactMapper.selectById(contactId);
-        if (contact == null || VppAuditHelper.isDeleted(contact.getDeleteFlag()) || !customerId.equals(contact.getCustomerId())) {
-            throw new BusinessException("联系人不存在");
-        }
-        return contact;
-    }
+    // ==================== 内部辅助方法 ====================
 
     private VppCustomer buildCustomerFromAccess(VppCustomerAccess access) {
         VppCustomer customer = new VppCustomer();
@@ -223,33 +942,284 @@ public class VppCustomerServiceImpl implements VppCustomerService {
         customer.setBusinessLicenseUrl(access.getBusinessLicenseUrl());
         customer.setLifecycleStatus(LIFECYCLE_ADMITTED);
         customer.setDrNotifyMinutes(30);
+        customer.setSigningDate(access.getSigningDate());
         return customer;
     }
 
-    private void validateAccessRequest(CustomerAccessRequest request) {
-        if (request == null || !StringUtils.hasText(request.getAccountNo())) {
-            throw new BusinessException("电力户号不能为空");
+    private CustomerResponseVO.ContractBriefVO toContractBrief(VppContract contract) {
+        CustomerResponseVO.ContractBriefVO brief = new CustomerResponseVO.ContractBriefVO();
+        brief.setId(contract.getId());
+        brief.setContractNo(contract.getContractNo());
+        brief.setContractName(contract.getContractName());
+        brief.setContractType(contract.getContractType());
+        brief.setContractStatus(contract.getContractStatus());
+        brief.setSignDate(contract.getSignDate());
+        brief.setEffectiveDate(contract.getEffectiveDate());
+        brief.setExpireDate(contract.getExpireDate());
+        return brief;
+    }
+
+    /**
+     * 租户内查询客户(含删除检查)
+     */
+    private VppCustomer findCustomerById(Long id) {
+        LambdaQueryWrapper<VppCustomer> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppCustomer::getId, id)
+                .eq(VppCustomer::getTenantId, SecurityUtils.getTenantId());
+        VppCustomer customer = customerMapper.selectOne(wrapper);
+        if (customer == null || VppAuditHelper.isDeleted(customer.getDeleteFlag())) {
+            throw new BusinessException("客户不存在或已被删除!");
+        }
+        return customer;
+    }
+
+    /**
+     * 租户内查询准入申请(含删除检查)
+     */
+    private VppCustomerAccess findAccessById(Long id) {
+        LambdaQueryWrapper<VppCustomerAccess> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppCustomerAccess::getId, id)
+                .eq(VppCustomerAccess::getTenantId, SecurityUtils.getTenantId());
+        VppCustomerAccess access = accessMapper.selectOne(wrapper);
+        if (access == null || VppAuditHelper.isDeleted(access.getDeleteFlag())) {
+            throw new BusinessException("准入申请不存在或已被删除!");
+        }
+        return access;
+    }
+
+    private VppCustomerContact getContact(Long customerId, Long contactId) {
+        LambdaQueryWrapper<VppCustomerContact> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(VppCustomerContact::getId, contactId)
+                .eq(VppCustomerContact::getCustomerId, customerId)
+                .eq(VppCustomerContact::getTenantId, SecurityUtils.getTenantId());
+        VppCustomerContact contact = contactMapper.selectOne(wrapper);
+        if (contact == null || VppAuditHelper.isDeleted(contact.getDeleteFlag())) {
+            throw new BusinessException("联系人不存在或已被删除!");
+        }
+        return contact;
+    }
+
+    private void validateAccessRequest(CustomerAccessRequestVO request) {
+        if (request == null || StringUtils.isBlank(request.getAccountNo())) {
+            throw new BusinessException("电力户号不能为空!");
         }
-        if (!StringUtils.hasText(request.getCustomerName())) {
-            throw new BusinessException("客户名称不能为空");
+        if (StringUtils.isBlank(request.getCustomerName())) {
+            throw new BusinessException("客户名称不能为空");
         }
         if (request.getCustomerType() == null) {
-            throw new BusinessException("客户类型不能为空");
+            throw new BusinessException("客户类型不能为空");
         }
-        if (!StringUtils.hasText(request.getPowerCompany())) {
-            throw new BusinessException("供电公司不能为空");
+        if (StringUtils.isBlank(request.getPowerCompany())) {
+            throw new BusinessException("供电公司不能为空");
         }
         if (request.getContractCapacity() == null) {
-            throw new BusinessException("用电容量不能为空");
+            throw new BusinessException("用电容量不能为空");
         }
     }
 
     private void validateContactRequest(CustomerContactRequest request) {
-        if (request == null || !StringUtils.hasText(request.getContactName())) {
-            throw new BusinessException("联系人姓名不能为空");
+        if (request == null || StringUtils.isBlank(request.getContactName())) {
+            throw new BusinessException("联系人姓名不能为空!");
+        }
+        if (StringUtils.isBlank(request.getContactPhone())) {
+            throw new BusinessException("联系电话不能为空!");
+        }
+        VppFieldValidator.validatePhone(request.getContactPhone());
+        if (StringUtils.isNotBlank(request.getContactEmail())) {
+            VppFieldValidator.validateEmail(request.getContactEmail());
         }
-        if (!StringUtils.hasText(request.getContactPhone())) {
-            throw new BusinessException("联系电话不能为空");
+    }
+
+    // ==================== Excel 导出辅助方法 ====================
+
+    private static void setCell(org.apache.poi.xssf.usermodel.XSSFRow row, int col, Object value) {
+        if (value != null) {
+            row.createCell(col).setCellValue(String.valueOf(value));
+        } else {
+            row.createCell(col).setCellValue("");
         }
     }
+
+    /**
+     * 下载营业执照图片并嵌入到 Excel 单元格中
+     *
+     * @param workbook 当前工作簿
+     * @param sheet    当前工作表
+     * @param row      当前行
+     * @param col      目标列索引
+     * @param imageUrl 营业执照图片 URL,为空或无效时写入"无"
+     */
+    private void insertLicenseImage(org.apache.poi.xssf.usermodel.XSSFWorkbook workbook,
+                                    org.apache.poi.xssf.usermodel.XSSFSheet sheet,
+                                    org.apache.poi.xssf.usermodel.XSSFRow row,
+                                    int col, String imageUrl) {
+        if (org.apache.commons.lang3.StringUtils.isBlank(imageUrl)) {
+            row.createCell(col).setCellValue("无");
+            return;
+        }
+
+        log.info("开始下载营业执照图片, URL: {}", imageUrl);
+
+        // 如果是相对路径(以 / 开头),尝试补全为本地文件路径或跳过
+        if (imageUrl.startsWith("/")) {
+            log.warn("营业执照URL是相对路径,无法通过HTTP下载: {}", imageUrl);
+            row.createCell(col).setCellValue("无");
+            return;
+        }
+
+        // 校验 URL 格式
+        if (!imageUrl.startsWith("http://") && !imageUrl.startsWith("https://")) {
+            log.warn("营业执照URL格式不正确: {}", imageUrl);
+            row.createCell(col).setCellValue("无");
+            return;
+        }
+
+        byte[] imageBytes = null;
+        try {
+            imageBytes = downloadImage(imageUrl);
+            log.info("图片下载成功, 大小: {} bytes, URL: {}", imageBytes.length, imageUrl);
+        } catch (Exception e) {
+            log.error("下载营业执照图片失败, URL: {}, 错误: {}", imageUrl, e.getMessage(), e);
+            row.createCell(col).setCellValue("加载失败");
+            return;
+        }
+
+        if (imageBytes == null || imageBytes.length == 0) {
+            row.createCell(col).setCellValue("无");
+            return;
+        }
+
+        try {
+            int pictureType = detectImageType(imageBytes);
+            log.info("图片类型检测结果: {}, URL: {}", pictureType, imageUrl);
+            int pictureIdx = workbook.addPicture(imageBytes, pictureType);
+
+            org.apache.poi.xssf.usermodel.XSSFDrawing drawing = sheet.createDrawingPatriarch();
+            int emuPerPixel = org.apache.poi.util.Units.EMU_PER_PIXEL;
+            // 全参构造:dx1, dy1, dx2, dy2, col1, row1, col2, row2
+            // dx/dy 设 2 像素微边距,图片紧贴单元格边界
+            org.apache.poi.xssf.usermodel.XSSFClientAnchor anchor =
+                    new org.apache.poi.xssf.usermodel.XSSFClientAnchor(
+                            2 * emuPerPixel, 2 * emuPerPixel,   // dx1, dy1 左/上边距
+                            2 * emuPerPixel, 2 * emuPerPixel,   // dx2, dy2 右/下边距
+                            col, row.getRowNum(),                // col1, row1
+                            col + 1, row.getRowNum() + 1         // col2, row2
+                    );
+            anchor.setAnchorType(org.apache.poi.ss.usermodel.ClientAnchor.AnchorType.MOVE_AND_RESIZE);
+
+            drawing.createPicture(anchor, pictureIdx);
+
+            if (row.getHeight() < 2000) {
+                row.setHeight((short) 2000);
+            }
+        } catch (Exception e) {
+            log.error("营业执照图片插入Excel失败, URL: {}, 错误: {}", imageUrl, e.getMessage(), e);
+            row.createCell(col).setCellValue("图片插入失败");
+        }
+    }
+
+    /**
+     * 通过 HTTP 下载图片,返回字节数组
+     */
+    private static byte[] downloadImage(String imageUrl) throws IOException {
+        URL url = new URL(imageUrl);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setConnectTimeout(10000);
+        conn.setReadTimeout(30000);
+        conn.setRequestMethod("GET");
+        conn.setDoInput(true);
+        conn.setInstanceFollowRedirects(true);
+        conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
+        conn.connect();
+
+        int responseCode = conn.getResponseCode();
+        if (responseCode != HttpURLConnection.HTTP_OK) {
+            conn.disconnect();
+            throw new IOException("下载图片失败,HTTP " + responseCode + ", URL: " + imageUrl);
+        }
+
+        String contentType = conn.getContentType();
+        if (contentType != null && !contentType.startsWith("image/")) {
+            conn.disconnect();
+            throw new IOException("返回的不是图片类型: " + contentType + ", URL: " + imageUrl);
+        }
+
+        try (InputStream is = conn.getInputStream();
+             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+            byte[] buffer = new byte[8192];
+            int len;
+            while ((len = is.read(buffer)) != -1) {
+                bos.write(buffer, 0, len);
+            }
+            return bos.toByteArray();
+        } finally {
+            conn.disconnect();
+        }
+    }
+
+    /**
+     * 根据图片字节前几个魔术字节判断图片类型
+     */
+    private static int detectImageType(byte[] bytes) {
+        if (bytes == null || bytes.length < 3) {
+            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG;
+        }
+        // PNG: 89 50 4E 47
+        if (bytes[0] == (byte) 0x89 && bytes[1] == (byte) 0x50
+                && bytes[2] == (byte) 0x4E && bytes[3] == (byte) 0x47) {
+            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_PNG;
+        }
+        // JPEG: FF D8 FF
+        if (bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xD8 && bytes[2] == (byte) 0xFF) {
+            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG;
+        }
+        // GIF: 47 49 46
+        if (bytes[0] == (byte) 0x47 && bytes[1] == (byte) 0x49 && bytes[2] == (byte) 0x46) {
+            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG; // POI 4.x 用 JPEG 兜底
+        }
+        // BMP: 42 4D
+        if (bytes[0] == (byte) 0x42 && bytes[1] == (byte) 0x4D) {
+            return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG; // POI 4.x 用 JPEG 兜底
+        }
+        return org.apache.poi.ss.usermodel.Workbook.PICTURE_TYPE_JPEG;
+    }
+
+    private static String typeLabel(Integer code, String... labels) {
+        if (code == null || code < 1 || code > labels.length) {
+            return "";
+        }
+        return labels[code - 1];
+    }
+
+    private static String accessStatusLabel(Integer code) {
+        if (code == null || code < 0 || code > 2) {
+            return "";
+        }
+        switch (code) {
+            case 0:
+                return "待审核";
+            case 1:
+                return "已通过";
+            case 2:
+                return "已驳回";
+            default:
+                return "";
+        }
+    }
+
+    private static String numberStr(Object value) {
+        return value == null ? "" : value.toString();
+    }
+
+    private static String timeStr(Object value) {
+        return value == null ? "" : value.toString();
+    }
+
+    private static String dateStr(Object value) {
+        return value == null ? "" : value.toString();
+    }
+
+    private static String dateTimeStr(Object value) {
+        return value == null ? "" : value.toString();
+    }
 }

+ 3 - 2
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerAccessAuditRequest.java

@@ -8,6 +8,7 @@ import lombok.Data;
 @Data
 public class CustomerAccessAuditRequest {
 
-    private Boolean passed;
-    private String auditOpinion;
+    private Long id;
+    private Integer result;
+    private String comment;
 }

+ 73 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerAccessRequestVO.java

@@ -0,0 +1,73 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 准入申请 请求VO
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
+ */
+@Data
+public class CustomerAccessRequestVO {
+
+    /**
+     * 电力户号
+     */
+    private String accountNo;
+
+    /**
+     * 客户名称
+     */
+    private String customerName;
+
+    /**
+     * 客户类型:1中高压 2低压商用 3居民充电桩 4自有资产
+     */
+    private Integer customerType;
+
+    /**
+     * 供电公司
+     */
+    private String powerCompany;
+
+    /**
+     * 签约容量
+     */
+    private BigDecimal contractCapacity;
+
+    /**
+     * 营业执照URL
+     */
+    private String businessLicenseUrl;
+
+    /**
+     * 信用状态:1正常 2关注 3不良
+     */
+    private Integer creditStatus;
+
+    /**
+     * 签约日期
+     */
+    private LocalDateTime signingDate;
+
+    /**
+     * 联系人姓名
+     */
+    private String contactName;
+
+    /**
+     * 联系电话
+     */
+    private String contactPhone;
+
+    /**
+     * 联系邮箱
+     */
+    private String contactEmail;
+}

+ 109 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerAccessResponseVO.java

@@ -0,0 +1,109 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 准入申请 响应VO
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
+ */
+@Data
+public class CustomerAccessResponseVO {
+
+    private Long id;
+
+    /**
+     * 申请编号
+     */
+    private String applyNo;
+
+    /**
+     * 电力户号
+     */
+    private String accountNo;
+
+    /**
+     * 客户名称
+     */
+    private String customerName;
+
+    /**
+     * 客户类型:1中高压 2低压商用 3居民充电桩 4自有资产
+     */
+    private Integer customerType;
+
+    /**
+     * 供电公司
+     */
+    private String powerCompany;
+
+    /**
+     * 签约容量
+     */
+    private BigDecimal contractCapacity;
+
+    /**
+     * 营业执照URL
+     */
+    private String businessLicenseUrl;
+
+    /**
+     * 信用状态:1正常 2关注 3不良
+     */
+    private Integer creditStatus;
+
+    /**
+     * 准入状态:0待审核 1通过 2拒绝
+     */
+    private Integer accessStatus;
+
+    /**
+     * 审核意见
+     */
+    private String auditOpinion;
+
+    /**
+     * 审核时间
+     */
+    private LocalDateTime auditAt;
+
+    /**
+     * 关联客户ID(审核通过后生成)
+     */
+    private Long customerId;
+
+    /**
+     * 申请时间
+     */
+    private LocalDateTime applyAt;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    private String createdBy;
+
+    /**
+     * 更新人
+     */
+    private String updatedBy;
+}

+ 174 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerRequestVO.java

@@ -0,0 +1,174 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+/**
+ * 客户管理 请求VO(用于更新客户信息)
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
+ */
+@Data
+public class CustomerRequestVO {
+
+    /**
+     * 主键ID
+     */
+    private Long id;
+
+    /**
+     * 电力户号(新增时必填)
+     */
+    private String accountNo;
+
+    /**
+     * 客户名称
+     */
+    private String customerName;
+
+    /**
+     * 客户类型:1中高压 2低压商用 3居民充电桩 4自有资产
+     */
+    private Integer customerType;
+
+    /**
+     * 供电公司
+     */
+    private String powerCompany;
+
+    /**
+     * 签约容量
+     */
+    private BigDecimal contractCapacity;
+
+    /**
+     * 信用状态:1正常 2关注 3不良
+     */
+    private Integer creditStatus;
+
+    /**
+     * 签约状态:1待签约 2已签约 3履约中 4待续约 5已续约 6已解约
+     */
+    private Integer lifecycleStatus;
+
+    /**
+     * 需求响应提前通知分钟数,默认30
+     */
+    private Integer drNotifyMinutes;
+
+    /**
+     * 需求响应上调能力 kW
+     */
+    private BigDecimal drUpCapacityKw;
+
+    /**
+     * 需求响应下调能力 kW
+     */
+    private BigDecimal drDownCapacityKw;
+
+    /**
+     * 省
+     */
+    private String province;
+
+    /**
+     * 市
+     */
+    private String city;
+
+    /**
+     * 区/县
+     */
+    private String district;
+
+    /**
+     * 地址
+     */
+    private String address;
+
+    /**
+     * 营业执照URL
+     */
+    private String businessLicenseUrl;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 是否虚拟电厂资源:0否 1是
+     */
+    private Integer isVppResource;
+
+    /**
+     * 虚拟电厂分类:1充换电 2分布式三联供 3楼宇空调 5用户侧储能 6数据中心 7工业负荷 8其他
+     */
+    private String vppCategory;
+
+    /**
+     * 需求响应资源分类:1工业负荷 2商业负荷 3储能 4电动汽车 5居民负荷 6其他
+     */
+    private String drResourceType;
+
+    /**
+     * 聚合代理用户虚拟电厂编码
+     */
+    private String vppProxyCode;
+
+    /**
+     * 所属行业:1制造业 2建筑业 3批发和零售业 ... 21其他
+     */
+    private String industry;
+
+    /**
+     * 所属产业:1第一产业 2第二产业 3第三产业
+     */
+    private String industrySector;
+
+    /**
+     * 运行容量(kW)
+     */
+    private BigDecimal runningCapacity;
+
+    /**
+     * 生产经营开始时间
+     */
+    private LocalTime productionStartDate;
+
+    /**
+     * 生产经营结束时间
+     */
+    private LocalTime productionEndDate;
+
+    /**
+     * 用电地址
+     */
+    private String powerAddress;
+
+    /**
+     * 签约日期
+     */
+    private LocalDateTime signingDate;
+
+    /**
+     * 联系人姓名
+     */
+    private String contactName;
+
+    /**
+     * 联系电话
+     */
+    private String contactPhone;
+
+    /**
+     * 联系邮箱
+     */
+    private String contactEmail;
+}

+ 209 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/service/vo/CustomerResponseVO.java

@@ -0,0 +1,209 @@
+package com.usky.vpp.service.vo;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.List;
+
+/**
+ * 客户管理 响应VO(含关联合同信息)
+ *
+ * @author fyc
+ * @email yuchuan.fu@chinausky.com
+ * @date 2026/7/6
+ */
+@Data
+public class CustomerResponseVO {
+
+    // ========== 客户基础字段 ==========
+
+    private Long id;
+
+    private String accountNo;
+
+    private String customerName;
+
+    /**
+     * 客户类型:1中高压 2低压商用 3居民充电桩 4自有资产
+     */
+    private Integer customerType;
+
+    private String powerCompany;
+
+    private BigDecimal contractCapacity;
+
+    /**
+     * 信用状态:1正常 2关注 3不良
+     */
+    private Integer creditStatus;
+
+    /**
+     * 签约状态:1待签约 2已签约 3履约中 4待续约 5已续约 6已解约
+     */
+    private Integer lifecycleStatus;
+
+    /**
+     * 需求响应提前通知分钟数
+     */
+    private Integer drNotifyMinutes;
+
+    /**
+     * 需求响应上调能力 kW
+     */
+    private BigDecimal drUpCapacityKw;
+
+    /**
+     * 需求响应下调能力 kW
+     */
+    private BigDecimal drDownCapacityKw;
+
+    private String province;
+
+    private String city;
+
+    private String district;
+
+    private String address;
+
+    private String businessLicenseUrl;
+
+    private String remark;
+
+    /**
+     * 是否虚拟电厂资源:0否 1是
+     */
+    private Integer isVppResource;
+
+    /**
+     * 虚拟电厂分类
+     */
+    private String vppCategory;
+
+    /**
+     * 需求响应资源分类
+     */
+    private String drResourceType;
+
+    /**
+     * 聚合代理用户虚拟电厂编码
+     */
+    private String vppProxyCode;
+
+    /**
+     * 所属行业
+     */
+    private String industry;
+
+    /**
+     * 所属产业
+     */
+    private String industrySector;
+
+    /**
+     * 运行容量(kW)
+     */
+    private BigDecimal runningCapacity;
+
+    /**
+     * 生产经营开始时间
+     */
+    private LocalTime productionStartDate;
+
+    /**
+     * 生产经营结束时间
+     */
+    private LocalTime productionEndDate;
+
+    /**
+     * 用电地址
+     */
+    private String powerAddress;
+
+    /**
+     * 签约时间
+     */
+    private LocalDateTime signingDate;
+
+    // ========== 审计字段 ==========
+
+    private Integer tenantId;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+
+    private String createdBy;
+
+    private String updatedBy;
+
+    // ========== 关联合同信息 ==========
+
+    /**
+     * 关联合同列表
+     */
+    private List<ContractBriefVO> contracts;
+
+    // ========== 关联联系人信息 ==========
+
+    /**
+     * 主联系人姓名
+     */
+    private String contactName;
+
+    /**
+     * 主联系电话
+     */
+    private String contactPhone;
+
+    /**
+     * 主联系邮箱
+     */
+    private String contactEmail;
+
+    /**
+     * 合同简要信息(嵌套VO)
+     */
+    @Data
+    public static class ContractBriefVO {
+
+        private Long id;
+
+        /**
+         * 合同编号
+         */
+        private String contractNo;
+
+        /**
+         * 合同名称
+         */
+        private String contractName;
+
+        /**
+         * 合同类型:1购售电 2需求响应合作 3聚合代理 4服务代理 5居民充电桩 6自有资产
+         */
+        private Integer contractType;
+
+        /**
+         * 合同状态:0草稿 1审核中 2已生效 3已到期 4已终止
+         */
+        private Integer contractStatus;
+
+        /**
+         * 签约日期
+         */
+        private LocalDate signDate;
+
+        /**
+         * 生效日期
+         */
+        private LocalDate effectiveDate;
+
+        /**
+         * 到期日期
+         */
+        private LocalDate expireDate;
+    }
+}

+ 48 - 0
service-vpp/service-vpp-biz/src/main/java/com/usky/vpp/util/VppFieldValidator.java

@@ -21,6 +21,16 @@ public final class VppFieldValidator {
      */
     private static final Pattern TEMPLATE_CODE_PATTERN = Pattern.compile("^[A-Z]{1,5}$");
 
+    /**
+     * 中国大陆手机号正则:1 开头,第二位 3-9,共 11 位数字
+     */
+    private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
+
+    /**
+     * 邮箱正则
+     */
+    private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
+
     private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
 
     private VppFieldValidator() {
@@ -90,4 +100,42 @@ public final class VppFieldValidator {
     public static String generateContractNo(String templateCode) {
         return templateCode + "-" + System.currentTimeMillis();
     }
+
+    /**
+     * 校验中国大陆手机号:
+     * <ul>
+     *   <li>不允许为空</li>
+     *   <li>1 开头,第二位 3-9,共 11 位数字</li>
+     * </ul>
+     *
+     * @param phone 手机号码
+     * @throws BusinessException 校验不通过时抛出
+     */
+    public static void validatePhone(String phone) {
+        if (StringUtils.isBlank(phone)) {
+            throw new BusinessException("手机号码不能为空!");
+        }
+        if (!PHONE_PATTERN.matcher(phone.trim()).matches()) {
+            throw new BusinessException("手机号码格式不正确!");
+        }
+    }
+
+    /**
+     * 校验邮箱格式:
+     * <ul>
+     *   <li>不允许为空</li>
+     *   <li>必须符合基本邮箱格式</li>
+     * </ul>
+     *
+     * @param email 邮箱地址
+     * @throws BusinessException 校验不通过时抛出
+     */
+    public static void validateEmail(String email) {
+        if (StringUtils.isBlank(email)) {
+            throw new BusinessException("邮箱不能为空!");
+        }
+        if (!EMAIL_PATTERN.matcher(email.trim()).matches()) {
+            throw new BusinessException("邮箱格式不正确!");
+        }
+    }
 }