diff --git a/build.gradle b/build.gradle index 5fd884b..ff10a31 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ repositories { } dependencies { + implementation 'org.hibernate:hibernate-java8:5.1.0.Final' compile "org.springframework.boot:spring-boot-starter:${springBootVersion}" compile 'org.springframework.boot:spring-boot-starter-web' compile 'org.springframework.boot:spring-boot-starter-freemarker' diff --git a/src/main/java/cn/palmte/work/controller/backend/ProcessController.java b/src/main/java/cn/palmte/work/controller/backend/ProcessController.java new file mode 100644 index 0000000..7afc235 --- /dev/null +++ b/src/main/java/cn/palmte/work/controller/backend/ProcessController.java @@ -0,0 +1,260 @@ +package cn.palmte.work.controller.backend; + +import org.springframework.beans.BeanUtils; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Controller; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.ui.Model; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.math.BigDecimal; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +import cn.palmte.work.model.Admin; +import cn.palmte.work.model.DeptRepository; +import cn.palmte.work.model.Project; +import cn.palmte.work.model.ProjectBudgetIncomeDetail; +import cn.palmte.work.model.ProjectRepository; +import cn.palmte.work.model.enums.CooperationType; +import cn.palmte.work.model.enums.Enumerable; +import cn.palmte.work.model.enums.ProcessStatus; +import cn.palmte.work.model.enums.ProcessType; +import cn.palmte.work.model.enums.ProjectType; +import cn.palmte.work.model.enums.SealType; +import cn.palmte.work.model.process.SaleContractProcess; +import cn.palmte.work.model.process.SealTypeArray; +import cn.palmte.work.model.process.form.SaleContractDetailForm; +import cn.palmte.work.model.process.form.SaleContractProcessForm; +import cn.palmte.work.service.ProjectBudgetService; +import cn.palmte.work.utils.InterfaceUtil; +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +/** + * @author Harry Yang + * @since 1.0 2022/12/8 11:03 + */ +@Controller +@RequiredArgsConstructor +@RequestMapping("/process") +public class ProcessController { + static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private final DeptRepository deptRepository; + private final ProjectRepository projectRepository; + private final ProjectBudgetService projectBudgetService; + + private final JdbcTemplate jdbcTemplate; + private final EntityManager entityManager; + + static class FormMetadata { + // 部门 + + // 印章类型 + public Map sealTypes; + + // 税率 + public List taxRate; + } + + @ResponseBody + @GetMapping("/form/metadata") + public FormMetadata metadata() { + FormMetadata metadata = new FormMetadata(); + metadata.sealTypes = Arrays.stream(SealType.values()) + .collect(Collectors.toMap(SealType::getDescription, Enum::name)); + metadata.taxRate = Arrays.asList(0, 1, 3, 4, 5, 6, 9, 10, 13); + return metadata; + } + + /** + * 新建流程 + */ + @GetMapping("/new") + public String newProcess(Model model) { + model.addAttribute("sealTypes", SealType.values()); + model.addAttribute("taxRate", Arrays.asList(0, 1, 3, 4, 5, 6, 9, 10, 13)); + return "/admin/business/process-new"; + } + + /** + * 已办流程 + */ + @GetMapping("/completed") + public String completed() { + return "/admin/business/process-completed"; + } + + /** + * 待我审核 + */ + @GetMapping("/review") + public String review(Model model) { + model.addAttribute("processTypes", ProcessType.values()); + return "/admin/business/process-review"; + } + + @ResponseBody + @GetMapping("/projects") + public List> query(@RequestParam String q) { + return projectRepository.findByProjectNoOrName(q) + .stream() + .map(project -> { + HashMap map = new HashMap<>(); + map.put("id", project.getId()); + map.put("name", project.getName()); + return map; + }) + .collect(Collectors.toList()); + } + + @Builder + public static class ProjectReturnValue { + public final String applyDate = LocalDate.now().format(formatter); + public String projectNo; + public Integer projectId; + public String projectName; + + // 申请人 + public String applyPersonName; + + // 项目类型 + public String projectType; + + // 合作类型 + public String cooperationType; + + // 合同金额 + public BigDecimal contractAmount; + + public List incomeDetails; + + // FIXME 垫资 + // 是否垫资 + public final String isPrepaid = "是"; + + // 垫资金额 + public final String repaidAmount = "50000元"; + + // 预算毛利率 + public final String budgetGrossMargin = "3.9%"; + } + + @ResponseBody + @GetMapping("/projects/{id}") + public ProjectReturnValue query(@PathVariable int id) { + Project project = projectRepository.findById(id); + Admin admin = InterfaceUtil.getAdmin(); + + // 可以在对应表数据查询 是否存在再启用 + List incomeDetails = projectBudgetService.getBudgetIncomeDetail(project); + return ProjectReturnValue.builder() + .projectId(project.getId()) + .incomeDetails(incomeDetails) + .projectName(project.getName()) + .projectNo(project.getProjectNo()) + .applyPersonName(admin.getRealName()) + .contractAmount(project.getContractAmount()) + .projectType(Enumerable.of(ProjectType.class, project.getType()).getDescription()) + .cooperationType(Enumerable.of(CooperationType.class, project.getCooperateType()).getDescription()) + .build(); + } + + // 销售合同流程 + + @ResponseBody + @PostMapping + @Transactional + public void post(@RequestBody @Valid SaleContractProcessForm form) { + SaleContractProcess entity = new SaleContractProcess(); + BeanUtils.copyProperties(form, entity, "sealTypes", "applyDate", "applyDept"); + entity.setApplyDate(LocalDate.parse(form.getApplyDate(), formatter)); + entity.setSealTypes(SealTypeArray.of(form.getSealTypes())); + entity.setApplyDept(String.join(",", form.getApplyDept())); + + entityManager.persist(entity); + + List incomeDetails = form.getIncomeDetails(); + if (!CollectionUtils.isEmpty(incomeDetails)) { + jdbcTemplate.batchUpdate("update project_budget_income_detail set expiration_date =? where id =? ", + new BatchPreparedStatementSetter() { + + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + SaleContractDetailForm detailForm = incomeDetails.get(i); + ps.setString(1, detailForm.getExpirationDate()); + ps.setInt(2, detailForm.getId()); + } + + @Override + public int getBatchSize() { + return incomeDetails.size(); + } + }); + } + } + + @Data + static class ProcessQueryForm { + + private String projectNo; + private String projectTitle; + private String applyPersonName; + + private ProcessType processType; + private ProcessStatus processStatus; + + } + + @ResponseBody + @PostMapping("/query") + public List list(@RequestBody ProcessQueryForm form) { + TypedQuery query = entityManager.createQuery( + "from SaleContractProcess ", SaleContractProcess.class); + +// query.setMaxResults(); + return query.getResultList(); + } + + // 审核 + @Data + static class AuditForm { + + private Integer processId; + + @NotNull + private String auditOpinion; + + private ProcessStatus processStatus; + + } + + @ResponseBody + @PostMapping("/audit") + public void audit(@RequestBody @Valid AuditForm form) { + + } + +} diff --git a/src/main/java/cn/palmte/work/model/ProjectBudgetIncomeDetailBase.java b/src/main/java/cn/palmte/work/model/ProjectBudgetIncomeDetailBase.java index 5760faf..4b24d21 100644 --- a/src/main/java/cn/palmte/work/model/ProjectBudgetIncomeDetailBase.java +++ b/src/main/java/cn/palmte/work/model/ProjectBudgetIncomeDetailBase.java @@ -37,6 +37,10 @@ public class ProjectBudgetIncomeDetailBase { @Column(name = "tax_rate") private BigDecimal taxRate; + // 质保期 5 个字符 + @Column(name = "expiration_date") + public String expirationDate; + public Integer getId() { return id; } @@ -117,6 +121,14 @@ public class ProjectBudgetIncomeDetailBase { this.taxRate = taxRate; } + public String getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(String expirationDate) { + this.expirationDate = expirationDate; + } + public BigDecimal getTotalTaxInclude(){ if(null == price){ return null; diff --git a/src/main/java/cn/palmte/work/model/ProjectRepository.java b/src/main/java/cn/palmte/work/model/ProjectRepository.java index 7d359e2..76e21a4 100644 --- a/src/main/java/cn/palmte/work/model/ProjectRepository.java +++ b/src/main/java/cn/palmte/work/model/ProjectRepository.java @@ -3,6 +3,7 @@ package cn.palmte.work.model; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import javax.transaction.Transactional; import java.util.Date; @@ -38,4 +39,9 @@ public interface ProjectRepository extends JpaRepository { @Transactional(rollbackOn = Exception.class) @Query(value = "update project set approve_id=?, approve_name=? where approve_id = ?", nativeQuery = true) int batchUpdateApprove(int targetAdminId, String targetAdminName, int adminId); + + @Query(value = "select * from project where `project_no` like concat('%', :q, '%') or `name` like concat('%', :q, '%')", + nativeQuery = true) + List findByProjectNoOrName(@Param("q") String query); + } diff --git a/src/main/java/cn/palmte/work/model/enums/CooperationType.java b/src/main/java/cn/palmte/work/model/enums/CooperationType.java new file mode 100644 index 0000000..e62194f --- /dev/null +++ b/src/main/java/cn/palmte/work/model/enums/CooperationType.java @@ -0,0 +1,30 @@ +package cn.palmte.work.model.enums; + +/** + * 合作类型:1战略合作类,2非战略合作类 + * + * @author Harry Yang + * @since 2.0 2022/12/14 15:20 + */ +public enum CooperationType implements Enumerable { + + STRATEGIC_COOPERATION(1, "战略合作"), + NOT_STRATEGIC_COOPERATION(2, "非战略合作"); + + private final int value; + private final String description; + + CooperationType(int value, String description) { + this.value = value; + this.description = description; + } + + public String getDescription() { + return description; + } + + @Override + public Integer getValue() { + return value; + } +} diff --git a/src/main/java/cn/palmte/work/model/enums/Descriptive.java b/src/main/java/cn/palmte/work/model/enums/Descriptive.java new file mode 100644 index 0000000..ed1a9d2 --- /dev/null +++ b/src/main/java/cn/palmte/work/model/enums/Descriptive.java @@ -0,0 +1,13 @@ +package cn.palmte.work.model.enums; + +/** + * @author Harry Yang + * @since 2.0 + */ +public interface Descriptive { + /** + * Return a description + */ + String getDescription(); + +} \ No newline at end of file diff --git a/src/main/java/cn/palmte/work/model/enums/Enumerable.java b/src/main/java/cn/palmte/work/model/enums/Enumerable.java new file mode 100644 index 0000000..2e4ad20 --- /dev/null +++ b/src/main/java/cn/palmte/work/model/enums/Enumerable.java @@ -0,0 +1,107 @@ +package cn.palmte.work.model.enums; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Enumerable for {@link Enum} + * + * @author Harry Yang + * @since 2.0 + */ +public interface Enumerable extends Descriptive { + + @SuppressWarnings("unchecked") + default V getValue() { + return (V) name(); + } + + @Override + default String getDescription() { + return name(); + } + + /** + * The default name of the enumeration, this method does not need + * to be implemented, the enumeration class is automatically inherited + */ + String name(); + + /** + * Returns Enumerable by {@link Enumerable#getValue() enum value} + * + * @param enumerable enum + * @param value enumeration value + * @param enumeration type + * @param enumeration value type + * @return enumeration instance + * @throws NullPointerException if enumerable is {@code null} + * @see Enumerable#getValue() + */ + // @Nullable + static , V> T of(Class enumerable, /*@Nullable*/ V value) { + if (value != null) { + T[] enumConstants = enumerable.getEnumConstants(); + if (enumConstants != null) { + for (T constant : enumConstants) { + if (Objects.equals(value, constant.getValue())) { + return constant; + } + } + } + } + return null; + } + + /** + * Get the value corresponding to the name + * + * @param enumerable enumeration class + * @param name enumeration name + * @param enum type + * @param enumeration value type + * @return enumeration value + * @see Enumerable#getValue() + */ + // @Nullable + static , V> V getValue(Class enumerable, String name) { + T[] enumConstants = enumerable.getEnumConstants(); + if (enumConstants != null) { + for (T constant : enumConstants) { + if (Objects.equals(name, constant.name())) { + return constant.getValue(); + } + } + } + return null; + } + + /** + * @param enum type + * @param enumeration value type + * @param defaultValue default value + * @see #of(Class, V) + */ + static , V> T of(Class enumerable, V value, Supplier defaultValue) { + return find(enumerable, value).orElseGet(defaultValue); + } + + /** + * @param defaultValue default value + * @param enum type + * @param enumeration value type + * @see #of(Class, V) + */ + static , V> T of(Class enumerable, V value, T defaultValue) { + return find(enumerable, value).orElse(defaultValue); + } + + /** + * @return Optional of T + */ + static , V> Optional find(Class enumerable, V value) { + return Optional.ofNullable(of(enumerable, value)); + } + +} diff --git a/src/main/java/cn/palmte/work/model/enums/ProcessStatus.java b/src/main/java/cn/palmte/work/model/enums/ProcessStatus.java new file mode 100644 index 0000000..6f15fe0 --- /dev/null +++ b/src/main/java/cn/palmte/work/model/enums/ProcessStatus.java @@ -0,0 +1,28 @@ +package cn.palmte.work.model.enums; + +/** + * 审核状态 + * + * @author Harry Yang + * @since 2.0 2022/12/13 16:12 + */ +public enum ProcessStatus implements Enumerable { + + draft("草稿"), + to_be_audit("待审核"), + audit_passed("审核通过"), + audit_not_passed("审核不通过"), + completed("完成"); + + private final String description; + + ProcessStatus(String description) { + this.description = description; + } + + @Override + public String getDescription() { + return description; + } + +} diff --git a/src/main/java/cn/palmte/work/model/enums/ProcessTaxRate.java b/src/main/java/cn/palmte/work/model/enums/ProcessTaxRate.java new file mode 100644 index 0000000..9b5241c --- /dev/null +++ b/src/main/java/cn/palmte/work/model/enums/ProcessTaxRate.java @@ -0,0 +1,23 @@ +package cn.palmte.work.model.enums; + +/** + * 业务应用里面的税率 + * + * @author Harry Yang + * @since 2.0 2022/12/13 15:41 + */ +public enum ProcessTaxRate { + + ; + + private final String description; + + ProcessTaxRate(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + +} diff --git a/src/main/java/cn/palmte/work/model/enums/ProcessType.java b/src/main/java/cn/palmte/work/model/enums/ProcessType.java new file mode 100644 index 0000000..9f5f740 --- /dev/null +++ b/src/main/java/cn/palmte/work/model/enums/ProcessType.java @@ -0,0 +1,22 @@ +package cn.palmte.work.model.enums; + +/** + * @author Harry Yang + * @since 2.0 2022/12/19 16:47 + */ +public enum ProcessType implements Enumerable { + sale_contract("销售合同流程"), + business_procurement("业务采购流程"); + + private final String description; + + ProcessType(String description) { + this.description = description; + } + + @Override + public String getDescription() { + return description; + } + +} diff --git a/src/main/java/cn/palmte/work/model/enums/ProjectType.java b/src/main/java/cn/palmte/work/model/enums/ProjectType.java new file mode 100644 index 0000000..5b9b01a --- /dev/null +++ b/src/main/java/cn/palmte/work/model/enums/ProjectType.java @@ -0,0 +1,32 @@ +package cn.palmte.work.model.enums; + +/** + * 项目类型:1 工程集成类、2设备集成类、3战略合作类 + * + * @author Harry Yang + * @since 2.0 2022/12/14 15:10 + */ +public enum ProjectType implements Enumerable { + + ENGINEERING_INTEGRATION(1, "工程集成"), + DEVICE_INTEGRATION(2, "设备集成"), + STRATEGIC_COOPERATION(3, "战略合作"); + + private final int value; + private final String description; + + ProjectType(int value, String description) { + this.value = value; + this.description = description; + } + + public String getDescription() { + return description; + } + + @Override + public Integer getValue() { + return value; + } + +} diff --git a/src/main/java/cn/palmte/work/model/enums/SealType.java b/src/main/java/cn/palmte/work/model/enums/SealType.java new file mode 100644 index 0000000..9db8d3c --- /dev/null +++ b/src/main/java/cn/palmte/work/model/enums/SealType.java @@ -0,0 +1,42 @@ +package cn.palmte.work.model.enums; + +/** + * 印章类型 + * + * @author Harry Yang + * @since 2.0 2022/12/13 15:25 + */ +public enum SealType implements Enumerable { + + // "公章" + // "法人章" + // "合同专用章" + // "项目章" + // "法人签名章" + // "总经理签名章" + // "财务专用章" + // "电子合同章" + // "其他" + + official("公章"), + legal("法人章"), + contract_special("合同专用章"), + project("项目章"), + legal_person_signature("法人签名章"), + general_manager_signature("总经理签名章"), + financial("财务专用章"), + electronic_contract("电子合同章"), + other("其他"); + + private final String description; + + SealType(String description) { + this.description = description; + } + + @Override + public String getDescription() { + return description; + } + +} diff --git a/src/main/java/cn/palmte/work/model/process/SaleContractProcess.java b/src/main/java/cn/palmte/work/model/process/SaleContractProcess.java new file mode 100644 index 0000000..6e85569 --- /dev/null +++ b/src/main/java/cn/palmte/work/model/process/SaleContractProcess.java @@ -0,0 +1,96 @@ +package cn.palmte.work.model.process; + +import org.hibernate.annotations.GenericGenerator; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import cn.palmte.work.model.enums.ProcessStatus; +import lombok.Data; + +/** + * 销售合同流程 + * + * @author Harry Yang + * @since 2.0 2022/12/14 16:11 + */ +@Data +@Entity +@Table(name = "sale_contract_process") +public class SaleContractProcess { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @GenericGenerator(name = "persistenceGenerator", strategy = "increment") + private Integer id; + + private Integer projectId; + + // 项目编号 + private String projectNo; + + private LocalDate applyDate; + + // 项目标题 + private String projectTitle; + + // 申请人 + private String applyPersonName; + + // 申请部门 + private String applyDept; + + // 申请部门领导 + private String applyDeptLeaderName; + + // 申请人电话 + private String applyPersonPhone; + + // 合同编号 + private String contractNo; + + // 合同名称 + private String contractName; + + // 客户名称 + private String clientName; + + // 用印类型 + @Convert(converter = SealTypeArrayConverter.class) + private SealTypeArray sealTypes; + + // 税率 + private String taxRate; + + // 收款条件 + private String paymentTerms; + + // 状态 + @Enumerated(EnumType.STRING) + private ProcessStatus status; + + // 当前审核人 + private String currentAudit; + + // 最后更新时间 + private LocalDateTime lastUpdateAt; + + private LocalDateTime createAt; + + // 项目类型 +// @Enumerated(EnumType.STRING) +// private ProjectType projectType; + + // 合作类型 +// @Enumerated(EnumType.STRING) +// private CooperationType cooperationType; +} diff --git a/src/main/java/cn/palmte/work/model/process/SaleContractProcessRepository.java b/src/main/java/cn/palmte/work/model/process/SaleContractProcessRepository.java new file mode 100644 index 0000000..0897bc9 --- /dev/null +++ b/src/main/java/cn/palmte/work/model/process/SaleContractProcessRepository.java @@ -0,0 +1,11 @@ +package cn.palmte.work.model.process; + +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * @author Harry Yang + * @since 2.0 2022/12/14 17:13 + */ +public interface SaleContractProcessRepository extends JpaRepository { + +} diff --git a/src/main/java/cn/palmte/work/model/process/SealTypeArray.java b/src/main/java/cn/palmte/work/model/process/SealTypeArray.java new file mode 100644 index 0000000..0f90d38 --- /dev/null +++ b/src/main/java/cn/palmte/work/model/process/SealTypeArray.java @@ -0,0 +1,58 @@ +package cn.palmte.work.model.process; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import javax.persistence.Convert; + +import cn.palmte.work.model.enums.Enumerable; +import cn.palmte.work.model.enums.SealType; + +/** + * @author Harry Yang + * @since 2.0 2022/12/14 16:35 + */ +//@JsonSerialize +@Convert(converter = SealTypeArrayConverter.class) +public class SealTypeArray { + public static final SealTypeArray EMPTY = new SealTypeArray(); + + private final List sealTypes = new ArrayList<>(); + + public SealTypeArray() { } + + public SealTypeArray(SealType... sealTypes) { + Collections.addAll(this.sealTypes, sealTypes); + } + + public List getSealTypes() { + return sealTypes; + } + + public void setSealTypes(List sealTypes) { + this.sealTypes.clear(); + this.sealTypes.addAll(sealTypes); + } + + // + + public static SealTypeArray of(SealType... sealTypes) { + return new SealTypeArray(sealTypes); + } + + public static SealTypeArray of(String... sealTypes) { + return of(Arrays.stream(sealTypes) + .map(type -> Enumerable.of(SealType.class, type)) + .toArray(SealType[]::new)); + } + + public static SealTypeArray of(Collection sealTypes) { + SealTypeArray sealTypeArray = new SealTypeArray(); + sealTypeArray.sealTypes.addAll(sealTypes); + return sealTypeArray; + } + +} diff --git a/src/main/java/cn/palmte/work/model/process/SealTypeArrayConverter.java b/src/main/java/cn/palmte/work/model/process/SealTypeArrayConverter.java new file mode 100644 index 0000000..eb36c59 --- /dev/null +++ b/src/main/java/cn/palmte/work/model/process/SealTypeArrayConverter.java @@ -0,0 +1,44 @@ +package cn.palmte.work.model.process; + +import org.springframework.util.StringUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.persistence.AttributeConverter; + +import cn.palmte.work.model.enums.Enumerable; +import cn.palmte.work.model.enums.SealType; + +/** + * @author Harry Yang + * @since 2.0 2022/12/14 16:36 + */ +public class SealTypeArrayConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(SealTypeArray attribute) { + List sealTypes = attribute.getSealTypes(); + if (sealTypes.isEmpty()) { + return null; + } + return sealTypes.stream() + .map(SealType::getValue) + .collect(Collectors.joining(",")); + } + + @Override + public SealTypeArray convertToEntityAttribute(String dbData) { + return Optional.ofNullable(dbData) + .filter(StringUtils::hasText) + .map(data -> data.split(",")) + .map(Arrays::stream) + .map(stream -> stream.map(element -> Enumerable.of(SealType.class, element)) + .toArray(SealType[]::new)) + .map(SealTypeArray::of) + .orElse(SealTypeArray.EMPTY); + } + +} diff --git a/src/main/java/cn/palmte/work/model/process/form/SaleContractDetailForm.java b/src/main/java/cn/palmte/work/model/process/form/SaleContractDetailForm.java new file mode 100644 index 0000000..8e05fb6 --- /dev/null +++ b/src/main/java/cn/palmte/work/model/process/form/SaleContractDetailForm.java @@ -0,0 +1,17 @@ +package cn.palmte.work.model.process.form; + +import lombok.Data; + +/** + * @author Harry Yang + * @since 2.0 2022/12/14 17:17 + */ +@Data +public class SaleContractDetailForm { + + private int id; + + // 5 个字符 + private String expirationDate; + +} diff --git a/src/main/java/cn/palmte/work/model/process/form/SaleContractProcessForm.java b/src/main/java/cn/palmte/work/model/process/form/SaleContractProcessForm.java new file mode 100644 index 0000000..e1806e9 --- /dev/null +++ b/src/main/java/cn/palmte/work/model/process/form/SaleContractProcessForm.java @@ -0,0 +1,71 @@ +package cn.palmte.work.model.process.form; + +import java.time.LocalDate; +import java.util.List; + +import javax.validation.constraints.NotNull; + +import cn.palmte.work.model.enums.ProcessStatus; +import lombok.Data; + +/** + * 销售合同流程 + * + * @author Harry Yang + * @since 2.0 2022/12/14 17:05 + */ +@Data +public class SaleContractProcessForm { + + @NotNull + private Integer projectId; + + // 项目编号 + private String projectNo; + + private String applyDate; + + // 项目标题 + private String projectTitle; + + // 项目类型 +// private String projectType; + + // 合作类型 +// private String cooperationType; + + // 申请人 + private String applyPersonName; + + private String[] applyDept; + + // 申请部门领导 + + private String applyDeptLeaderName; + + // 申请人电话 + private String applyPersonPhone; + + // 合同编号 + private String contractNo; + + // 合同名称 + private String contractName; + + // 客户名称 + private String clientName; + + // 用印类型 + private String[] sealTypes; + + // 税率 + private String taxRate; + + // 收款条件 + private String paymentTerms; + + private ProcessStatus status; + + private List incomeDetails; + +} diff --git a/src/main/java/cn/palmte/work/model/process/package-info.java b/src/main/java/cn/palmte/work/model/process/package-info.java new file mode 100644 index 0000000..4c4a60c --- /dev/null +++ b/src/main/java/cn/palmte/work/model/process/package-info.java @@ -0,0 +1,7 @@ +/** + * 流程相关的 Model + * + * @author Harry Yang + * @since 2.0 2022/12/14 16:32 + */ +package cn.palmte.work.model.process; \ No newline at end of file diff --git a/src/main/java/cn/palmte/work/service/UploadService.java b/src/main/java/cn/palmte/work/service/UploadService.java deleted file mode 100644 index 92888e8..0000000 --- a/src/main/java/cn/palmte/work/service/UploadService.java +++ /dev/null @@ -1,73 +0,0 @@ -package cn.palmte.work.service; - -import cn.palmte.work.config.UploadProperties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import sun.misc.BASE64Decoder; -import top.jfunc.common.datetime.DatetimeUtils; -import top.jfunc.common.utils.CommonUtil; -import top.jfunc.common.utils.FileUtil; -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Date; - -/** - * @author xiongshiyan at 2020/10/13 , contact me with email yanshixiong@126.com or phone 15208384257 - */ -@Service -public class UploadService { - private static final Logger logger = LoggerFactory.getLogger(UploadService.class); - public static final String DATA_IMAGE_PNG_BASE64 = "data:image/png;base64,"; - public static final String DATA_IMAGE_JPEG_BASE64 = "data:image/jpeg;base64,"; - public static final String DATA_IMAGE_JPG_BASE64 = "data:image/jpg;base64,"; - - @Autowired - private UploadProperties uploadProperties; - - public String upload(String base64Str){ - String suffix = "png"; - if (base64Str.contains(DATA_IMAGE_PNG_BASE64)) { - base64Str = base64Str.replaceAll(DATA_IMAGE_PNG_BASE64, ""); - suffix = "png"; - } else if (base64Str.contains(DATA_IMAGE_JPEG_BASE64)) { - base64Str = base64Str.replaceAll(DATA_IMAGE_JPEG_BASE64, ""); - suffix = "jpeg"; - }else if (base64Str.contains(DATA_IMAGE_JPG_BASE64)) { - base64Str = base64Str.replaceAll(DATA_IMAGE_JPG_BASE64, ""); - suffix = "jpg"; - } - String uploadPath = uploadProperties.getPath(); - String uploadPrefix = uploadProperties.getPrefix(); - String yyyyMMdd = DatetimeUtils.toStr(new Date(), "yyyyMMdd"); - String saveDir = uploadPath + "/" + yyyyMMdd; - FileUtil.makeSureExistDir(saveDir); - - String fileName = CommonUtil.randomString(32) + "." + suffix; - String absolutePath = saveDir + "/" + fileName; - File file = new File(absolutePath); - - decodeAndWrite(base64Str , file); - - return uploadPrefix + "/" + yyyyMMdd + "/" + fileName; - } - - - private void decodeAndWrite(String base64Str, File destFile) { - try { - // Base64解码 - byte[] bytes = new BASE64Decoder().decodeBuffer(base64Str); - for (int i = 0; i < bytes.length; ++i) { - if (bytes[i] < 0) { - // 调整异常数据 - bytes[i] += 256; - } - } - Files.write(Paths.get(destFile.getAbsolutePath()), bytes); - } catch (Exception e) { - logger.error(e.getMessage(), e); - } - } -} diff --git a/src/main/java/cn/palmte/work/utils/image/VerifyImageUtil.java b/src/main/java/cn/palmte/work/utils/image/VerifyImageUtil.java deleted file mode 100644 index 9930bfc..0000000 --- a/src/main/java/cn/palmte/work/utils/image/VerifyImageUtil.java +++ /dev/null @@ -1,305 +0,0 @@ -package cn.palmte.work.utils.image; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sun.misc.BASE64Decoder; -import top.jfunc.common.utils.ArrayUtil; -import top.jfunc.common.utils.RandomUtil; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.*; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -import java.util.Random; - -/** - * 滑动验证码相关工具方法 - */ -public class VerifyImageUtil { - private static final Logger logger = LoggerFactory.getLogger(VerifyImageUtil.class); - /** - * 模板图宽度 - */ - private static int CUT_WIDTH = 50; - /** - * 模板图高度 - */ - private static int CUT_HEIGHT = 50; - /** - * 抠图凸起圆心 - */ - private static int CIRCLE_R = 5; - /** - * 抠图内部矩形填充大小 - */ - private static int RECTANGLE_PADDING = 8; - /** - * 抠图的边框宽度 - */ - private static int SLIDER_IMG_OUT_PADDING = 1; - - private static int CENTER_INDICATOR = 1; - private static int BORDER_INDICATOR = 2; - private static int OTHER_INDICATOR = 0; - - /** - * 根据传入的file生成验证码图片 - */ - public static VerifyImage getVerifyImage(File file) throws IOException { - BufferedImage srcImage = ImageIO.read(file); - - return getVerifyImage(srcImage); - } - - /** - * 根据转入的流生成验证码图片 - */ - public static VerifyImage getVerifyImage(InputStream inputStream) throws IOException { - BufferedImage srcImage = ImageIO.read(inputStream); - - return getVerifyImage(srcImage); - } - - /** - * 以左上角为计数起点 - */ - public static VerifyImage getVerifyImage(BufferedImage srcImage) throws IOException { - //1/4=75处,最右减1个方块儿,-5是保守的防止超界 - int locationX = RandomUtil.randomInt(srcImage.getWidth()/4, srcImage.getWidth()-CUT_WIDTH-5); - //距离最下方一个方块,-5是保守的防止超界 - int locationY = RandomUtil.randomInt(5, srcImage.getHeight()-CUT_HEIGHT-5); - - BufferedImage markImage = new BufferedImage(CUT_WIDTH,CUT_HEIGHT,BufferedImage.TYPE_4BYTE_ABGR); - cutImgByTemplate(srcImage, markImage, getBlockData(), locationX, locationY); - return new VerifyImage(getImageBASE64(srcImage), - getImageBASE64(markImage), - locationX, - locationY, - srcImage.getWidth(), - srcImage.getHeight()); - } - - - /** - * 生成随机滑块形状 - *

- * 0 透明像素 - * 1 滑块像素 - * 2 阴影像素 - * @return int[][] - */ - private static int[][] getBlockData() { - int[][] data = new int[CUT_WIDTH][CUT_HEIGHT]; - SecureRandom random = new SecureRandom(); - //Random random = new Random(); - //(x-a)²+(y-b)²=r² - //x中心位置左右5像素随机 - double x1 = RECTANGLE_PADDING + (CUT_WIDTH - 2 * RECTANGLE_PADDING) / 2.0/* - 5 + random.nextInt(10)*/; - //y 矩形上边界半径-1像素移动 - double y1_top = RECTANGLE_PADDING - random.nextInt(3); - double y1_bottom = CUT_HEIGHT - RECTANGLE_PADDING + random.nextInt(3); - double y1 = random.nextInt(2) == 1 ? y1_top : y1_bottom; - - - double x2_right = CUT_WIDTH - RECTANGLE_PADDING - CIRCLE_R + random.nextInt(2 * CIRCLE_R - 4); - double x2_left = RECTANGLE_PADDING + CIRCLE_R - 2 - random.nextInt(2 * CIRCLE_R - 4); - double x2 = random.nextInt(2) == 1 ? x2_right : x2_left; - double y2 = RECTANGLE_PADDING + (CUT_HEIGHT - 2 * RECTANGLE_PADDING) / 2.0 - 4 + random.nextInt(10); - - double po = Math.pow(CIRCLE_R, 2); - for (int i = 0; i < CUT_WIDTH; i++) { - for (int j = 0; j < CUT_HEIGHT; j++) { - //矩形区域 - boolean fill; - if ((i >= RECTANGLE_PADDING && i < CUT_WIDTH - RECTANGLE_PADDING) - && (j >= RECTANGLE_PADDING && j < CUT_HEIGHT - RECTANGLE_PADDING)) { - data[i][j] = CENTER_INDICATOR; - fill = true; - } else { - data[i][j] = OTHER_INDICATOR; - fill = false; - } - //凸出区域 - double d3 = Math.pow(i - x1, 2) + Math.pow(j - y1, 2); - if (d3 < po) { - data[i][j] = CENTER_INDICATOR; - } else { - if (!fill) { - data[i][j] = OTHER_INDICATOR; - } - } - //凹进区域 - double d4 = Math.pow(i - x2, 2) + Math.pow(j - y2, 2); - if (d4 < po) { - data[i][j] = OTHER_INDICATOR; - } - } - } - //边界阴影 - for (int i = 0; i < CUT_WIDTH; i++) { - for (int j = 0; j < CUT_HEIGHT; j++) { - //四个正方形边角处理 - for (int k = 1; k <= SLIDER_IMG_OUT_PADDING; k++) { - //左上、右上 - if (i >= RECTANGLE_PADDING - k && i < RECTANGLE_PADDING - && ((j >= RECTANGLE_PADDING - k && j < RECTANGLE_PADDING) - || (j >= CUT_HEIGHT - RECTANGLE_PADDING - k && j < CUT_HEIGHT - RECTANGLE_PADDING +1))) { - data[i][j] = BORDER_INDICATOR; - } - - //左下、右下 - if (i >= CUT_WIDTH - RECTANGLE_PADDING + k - 1 && i < CUT_WIDTH - RECTANGLE_PADDING + 1) { - for (int n = 1; n <= SLIDER_IMG_OUT_PADDING; n++) { - if (((j >= RECTANGLE_PADDING - n && j < RECTANGLE_PADDING) - || (j >= CUT_HEIGHT - RECTANGLE_PADDING - n && j <= CUT_HEIGHT - RECTANGLE_PADDING ))) { - data[i][j] = BORDER_INDICATOR; - } - } - } - } - - if (data[i][j] == 1 && j - SLIDER_IMG_OUT_PADDING > 0 && data[i][j - SLIDER_IMG_OUT_PADDING] == 0) { - data[i][j - SLIDER_IMG_OUT_PADDING] = BORDER_INDICATOR; - } - if (data[i][j] == 1 && j + SLIDER_IMG_OUT_PADDING > 0 && j + SLIDER_IMG_OUT_PADDING < CUT_HEIGHT && data[i][j + SLIDER_IMG_OUT_PADDING] == 0) { - data[i][j + SLIDER_IMG_OUT_PADDING] = BORDER_INDICATOR; - } - if (data[i][j] == 1 && i - SLIDER_IMG_OUT_PADDING > 0 && data[i - SLIDER_IMG_OUT_PADDING][j] == 0) { - data[i - SLIDER_IMG_OUT_PADDING][j] = BORDER_INDICATOR; - } - if (data[i][j] == 1 && i + SLIDER_IMG_OUT_PADDING > 0 && i + SLIDER_IMG_OUT_PADDING < CUT_WIDTH && data[i + SLIDER_IMG_OUT_PADDING][j] == 0) { - data[i + SLIDER_IMG_OUT_PADDING][j] = BORDER_INDICATOR; - } - } - } - - - //最外边界 - /*for (int i = 0; i < CUT_WIDTH; i++) { - data[0][i] = data[CUT_WIDTH-1][i] = 3; - } - for (int i = 0; i < CUT_HEIGHT; i++) { - data[i][0] = data[i][CUT_HEIGHT-1] = 3; - }*/ - - return data; - } - - /** - * 裁剪区块 - * 根据生成的滑块形状,对原图和裁剪块进行变色处理 - * @param oriImage 原图 - * @param targetImage 裁剪图 - * @param blockImage 滑块 - * @param x 裁剪点x - * @param y 裁剪点y - */ - private static void cutImgByTemplate(BufferedImage oriImage, BufferedImage targetImage, int[][] blockImage, int x, int y) { - - print(blockImage); - - for (int i = 0; i < CUT_WIDTH; i++) { - for (int j = 0; j < CUT_HEIGHT; j++) { - int xx = x + i; - int yy = y + j; - int rgbFlg = blockImage[i][j]; - int rgb_ori = oriImage.getRGB(xx, yy); - - // 原图中对应位置变色处理 - if (rgbFlg == CENTER_INDICATOR) { - //中心 - //抠图上复制对应颜色值 - targetImage.setRGB(i,j, rgb_ori); - //原图对应位置颜色变化[透明50%] - oriImage.setRGB(xx, yy, rgb_ori & 0x80ffffff); - } else if (rgbFlg == BORDER_INDICATOR) { - //边框 - targetImage.setRGB(i, j, Color.WHITE.getRGB()); - oriImage.setRGB(xx, yy, Color.GRAY.getRGB()); - }else if(rgbFlg == OTHER_INDICATOR){ - //int alpha = 0; - targetImage.setRGB(i, j, rgb_ori & 0x00ffffff); - } - } - - } - } - private static void print(int[][] blockData){ - StringBuilder builder = new StringBuilder("\r\n"); - for (int i = 0; i < blockData.length; i++) { - for (int j = 0; j < blockData[i].length; j++) { - builder.append(blockData[i][j]).append(" "); - } - builder.append("\r\n"); - } - logger.debug(builder.toString()); - } - - - /** - * 随机获取一张图片文件 - */ - public static File getRandomFile(File dir) throws IOException { - File[] fileList = dir.listFiles(); - if(ArrayUtil.isEmpty(fileList)){ - return null; - } - List fileNameList = new ArrayList<>(fileList.length); - for (File tempFile: fileList){ - if (tempFile.isDirectory()) { - continue; - } - if (tempFile.getName().endsWith(".png") || tempFile.getName().endsWith(".jpg")){ - fileNameList.add(tempFile.getAbsolutePath().trim()); - } - } - int randomIndex = new SecureRandom().nextInt(fileNameList.size()); - //int randomIndex = new Random().nextInt(fileNameList.size()); - return new File(fileNameList.get(randomIndex)); - } - - /** - * 将IMG输出为文件 - */ - public static void writeImg(BufferedImage image, File file) throws IOException { - ByteArrayOutputStream bao=new ByteArrayOutputStream(); - ImageIO.write(image,"png",bao); - try (FileOutputStream out = new FileOutputStream(file)){ - out.write(bao.toByteArray()); - } - } - - /** - * 将图片转换为BASE64 - */ - public static String getImageBASE64(BufferedImage image) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - ImageIO.write(image,"png",out); - //转成byte数组 - byte[] bytes = out.toByteArray(); - - return Base64.getEncoder().encodeToString(bytes); - } - - /** - * 将BASE64字符串转换为图片 - */ - public static BufferedImage base64String2Image(String base64String) throws IOException { - BASE64Decoder decoder=new BASE64Decoder(); - byte[] bytes1 = decoder.decodeBuffer(base64String); - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes1); - return ImageIO.read(byteArrayInputStream); - } - - /*public static void main(String[] args) throws IOException { - VerifyImage verifyImage = VerifyImageUtil.getVerifyImage(new File("C:\\Users\\xiongshiyan\\Desktop\\ssss.png")); - System.out.println(verifyImage); - VerifyImageUtil.writeImg(VerifyImageUtil.base64String2Image(verifyImage.getSrcImage()), new File("C:\\Users\\xiongshiyan\\Desktop\\src.png")); - VerifyImageUtil.writeImg(VerifyImageUtil.base64String2Image(verifyImage.getCutImage()), new File("C:\\Users\\xiongshiyan\\Desktop\\cut.png")); - }*/ - -} \ No newline at end of file diff --git a/src/main/resources/sql/2.0.sql b/src/main/resources/sql/2.0.sql new file mode 100644 index 0000000..f452615 --- /dev/null +++ b/src/main/resources/sql/2.0.sql @@ -0,0 +1,38 @@ +# 创建 流程表 +create table sale_contract_process +( + id int auto_increment primary key, + apply_date date null, + apply_dept varchar(255) null, + apply_dept_leader_name varchar(255) null, + apply_person_name varchar(255) null, + apply_person_phone varchar(255) null, + client_name varchar(255) null, + contract_name varchar(255) null, + contract_no varchar(255) null, + payment_terms varchar(255) null, + project_id int null, + project_no varchar(255) null, + project_title varchar(255) null comment '标题', + seal_types varchar(255) null comment '印章类型', + `status` varchar(255) null, + tax_rate varchar(255) null, + + current_audit varchar(255) null comment '当前审核人', + + create_at datetime default CURRENT_TIMESTAMP comment '创建时间', + last_update_at datetime on update CURRENT_TIMESTAMP comment '最后更新时间' + +); + + +alter table sale_contract_process + add current_audit varchar(255) null comment '当前审核人'; + +alter table sale_contract_process + add create_at datetime default CURRENT_TIMESTAMP comment '创建时间'; +alter table sale_contract_process + add last_update_at datetime on update CURRENT_TIMESTAMP comment '最后更新时间'; + + + diff --git a/src/main/resources/static/assets/process/业务采购流程@2x.png b/src/main/resources/static/assets/process/业务采购流程@2x.png new file mode 100644 index 0000000..32ec71c Binary files /dev/null and b/src/main/resources/static/assets/process/业务采购流程@2x.png differ diff --git a/src/main/resources/static/assets/process/业务采购流程@3x.png b/src/main/resources/static/assets/process/业务采购流程@3x.png new file mode 100644 index 0000000..a17a35b Binary files /dev/null and b/src/main/resources/static/assets/process/业务采购流程@3x.png differ diff --git a/src/main/resources/static/assets/process/销售合同流程@2x.png b/src/main/resources/static/assets/process/销售合同流程@2x.png new file mode 100644 index 0000000..1899213 Binary files /dev/null and b/src/main/resources/static/assets/process/销售合同流程@2x.png differ diff --git a/src/main/resources/static/assets/process/销售合同流程@3x.png b/src/main/resources/static/assets/process/销售合同流程@3x.png new file mode 100644 index 0000000..2ab1d02 Binary files /dev/null and b/src/main/resources/static/assets/process/销售合同流程@3x.png differ diff --git a/src/main/resources/templates/admin/business/process-completed.ftl b/src/main/resources/templates/admin/business/process-completed.ftl new file mode 100644 index 0000000..f82c5c9 --- /dev/null +++ b/src/main/resources/templates/admin/business/process-completed.ftl @@ -0,0 +1,44 @@ +<#assign base=request.contextPath /> +<#import "../../common/defaultLayout.ftl" as defaultLayout> +<@defaultLayout.layout> + + +

{{ message }}
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/templates/admin/business/process-new.ftl b/src/main/resources/templates/admin/business/process-new.ftl new file mode 100644 index 0000000..a2d9463 --- /dev/null +++ b/src/main/resources/templates/admin/business/process-new.ftl @@ -0,0 +1,855 @@ +<#assign base=request.contextPath /> +<#import "../../common/defaultLayout.ftl" as defaultLayout> +<@defaultLayout.layout> +<#-- --> + + + +
+
+
+
业务应用 / + {{subTitle}}
+
+ +
+
+ + + +
+ + <#-- 业务采购流程 --> +
+
+
项目编号
+
+ +
+ +
+
+ + <#-- 新增销售合同流程 --> + +
+ + +
+ + + + + + + + + + {{projectTitle}} + 未选择项目 + + + + + {{processForm.applyDate}} + 未选择项目 + + + + {{processForm.projectType}} + 未选择项目 + + + + {{processForm.cooperationType}} + 未选择项目 + + +
+ +
+ + + + + + + {{processForm.applyPersonName}} + 未选择项目 + + + + {{processForm.applyDeptLeaderName}} + 未选择部门 + + + + + + +
+ +
+ + + + + + + + + + + {{processForm.contractAmount}}元 + 未选择项目 + + +
+ +
+ + + + + + <#--TODO 最终用户名称--> + {{processForm.finalUserName}} + 未选择项目 + +
+ +
+ + + <#list sealTypes as sealType> + ${sealType.description} + + + +
+ +
+ + + + <#list taxRate as rate> + + + + + + + {{processForm.isPrepaid}} + 未选择项目 + + + + {{processForm.repaidAmount}} + 未选择项目 + + + + {{processForm.budgetGrossMargin}} + 未选择项目 + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + 点击上传 +
只能上传PDF、excel、word、图片、压缩包,且不超过50MB
+
+
+
+ +
+ + 详细清单 + +
+ +
+ + <#-- --> + + 返回上一级 + 保存草稿 + 提交 + + + +
+ + <#-- 销售合同清单明细 --> + +
+ + + + + + + + + + + + + + + + + + + + + + 返回上一级 + 保存并返回上一级 + + +
+ + <#-- 新增业务采购合同流程 --> + +
+
+
+ +
+ + + + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ +
+ + + + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + <#-- 选择 业务采购清单明细 --> + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/templates/admin/business/process-review.ftl b/src/main/resources/templates/admin/business/process-review.ftl new file mode 100644 index 0000000..1390c37 --- /dev/null +++ b/src/main/resources/templates/admin/business/process-review.ftl @@ -0,0 +1,299 @@ +<#assign base=request.contextPath /> +<#import "../../common/defaultLayout.ftl" as defaultLayout> +<@defaultLayout.layout> + + + +
+
+
+
业务应用 / + 待我审核
+
+ +
+
+ + +
+ + + + + + + + + + + + <#list processTypes as processType> + + + + + + + + + + + 查询 +
+ +
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + 审核通过 + 审核不通过 + + + + + + + + + + + + + +
+ +
+ + + + + + + +