feat: 设施价格表(四部门会签 + 生效日期 + 实例即版本)#57
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
-- 设施价格表(设施损坏修复与租用价格表)业务表(可重复执行)
|
||||
-- 目标库:db_gxtyzx(master)
|
||||
-- 执行顺序:05-approval_phase2.sql -> ... -> 11-approval-facility.sql
|
||||
--
|
||||
-- ADR 0012:审批实例即版本,不建独立价格表主数据表。
|
||||
-- approval_facility_form 每个审批实例一行,业务字段只有生效日期(必填)
|
||||
-- approval_facility_item 实例的明细行,行即该版本条目,金额服务端重算
|
||||
--
|
||||
-- 当前生效版本 = 同租户同 flow_code 下 status=ARCHIVED 且 effective_date <= today
|
||||
-- 中 effective_date 最大者;历史版本按 effective_date 倒序(查询时算,不落库)。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS approval_facility_form (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id INT NOT NULL DEFAULT 10150,
|
||||
instance_id BIGINT NOT NULL,
|
||||
effective_date DATE NOT NULL COMMENT '生效日期,不允许早于归档日',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_instance (instance_id),
|
||||
KEY idx_effective (tenant_id, effective_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设施价格表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS approval_facility_item (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
tenant_id INT NOT NULL DEFAULT 10150,
|
||||
instance_id BIGINT NOT NULL,
|
||||
line_no INT NOT NULL COMMENT '行号,按提交顺序 1..N',
|
||||
item_name VARCHAR(200) NOT NULL COMMENT '名称',
|
||||
location VARCHAR(200) DEFAULT NULL COMMENT '位置',
|
||||
unit VARCHAR(20) NOT NULL COMMENT '单位',
|
||||
quantity DECIMAL(12,3) NOT NULL COMMENT '数量',
|
||||
unit_price DECIMAL(12,2) NOT NULL COMMENT '单价',
|
||||
amount DECIMAL(12,2) NOT NULL COMMENT '金额,服务端按 quantity × unit_price 重算',
|
||||
remark VARCHAR(200) DEFAULT NULL COMMENT '备注',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_instance_line (instance_id, line_no),
|
||||
KEY idx_instance (instance_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设施价格表明细';
|
||||
@@ -0,0 +1,313 @@
|
||||
package com.gxwebsoft.approval.doc.facility;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.gxwebsoft.approval.constant.ApprovalConstants;
|
||||
import com.gxwebsoft.approval.document.AbstractApprovalDocumentType;
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentParam;
|
||||
import com.gxwebsoft.approval.document.ApprovalLineSupport;
|
||||
import com.gxwebsoft.approval.entity.ApprovalFacilityForm;
|
||||
import com.gxwebsoft.approval.entity.ApprovalFacilityItem;
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalFacilityFormMapper;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalFacilityItemMapper;
|
||||
import com.gxwebsoft.approval.param.ApprovalFacilityParam;
|
||||
import com.gxwebsoft.approval.support.ApprovalLineSpec;
|
||||
import com.gxwebsoft.approval.vo.ApprovalDetail;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 设施价格表(设施损坏修复与租用价格表,ADR 0012):
|
||||
* 报送人 → 报送部门负责人 → 四部门会签 → 分管领导 → 总经理 → 存档(报送人 + 运营管理部)→ 归档。
|
||||
*
|
||||
* <p>只做价格目录,不含损坏描述/报修时间/修复内容——那是价格表被使用的场景,不是第二张单。
|
||||
* 审批实例即版本:每次归档生成一版,明细行即该版条目;不建独立价格表主数据表。</p>
|
||||
*
|
||||
* <p>生效日期允许未来(提前批),不允许早于当天(追溯修改会让历史结算失去依据):
|
||||
* 草稿保存即拒绝早于今天的日期,归档前由 {@link #validateBeforeArchive} 再校验一次。</p>
|
||||
*/
|
||||
@Component
|
||||
public class FacilityDocumentType extends AbstractApprovalDocumentType {
|
||||
|
||||
private static final String NODES_JSON = "["
|
||||
+ "{\"key\":\"reportLeader\",\"type\":\"APPROVE\",\"resolver\":\"reporterOrgLeader\","
|
||||
+ "\"name\":\"报送部门负责人\"},"
|
||||
+ "{\"key\":\"deptCountersign\",\"type\":\"COUNTERSIGN\",\"name\":\"四部门会签\",\"groups\":["
|
||||
+ "{\"key\":\"venue\",\"name\":\"场馆部\",\"resolver\":\"orgLeaderByCode\",\"orgCode\":\"APPROVAL_VENUE\"},"
|
||||
+ "{\"key\":\"ops\",\"name\":\"运营管理部\",\"resolver\":\"orgLeaderByCode\",\"orgCode\":\"APPROVAL_OPS\"},"
|
||||
+ "{\"key\":\"safety\",\"name\":\"安全运保部\",\"resolver\":\"orgLeaderByCode\",\"orgCode\":\"APPROVAL_SAFETY\"},"
|
||||
+ "{\"key\":\"finance\",\"name\":\"财务部\",\"resolver\":\"orgLeaderByCode\",\"orgCode\":\"APPROVAL_FINANCE\"}"
|
||||
+ "]},"
|
||||
+ "{\"key\":\"deputyLeader\",\"type\":\"APPROVE\",\"resolver\":\"reporterOrgDeputy\","
|
||||
+ "\"name\":\"分管领导\"},"
|
||||
+ "{\"key\":\"generalManager\",\"type\":\"APPROVE\",\"resolver\":\"roleGeneralManager\","
|
||||
+ "\"name\":\"总经理\"},"
|
||||
+ "{\"key\":\"confirm\",\"type\":\"CONFIRM\",\"name\":\"存档\",\"groups\":["
|
||||
+ "{\"key\":\"reporter\",\"name\":\"报送人\",\"resolver\":\"reporter\"},"
|
||||
+ "{\"key\":\"ops\",\"name\":\"运营管理部\",\"resolver\":\"roleTypist\"}"
|
||||
+ "]},"
|
||||
+ "{\"key\":\"archive\",\"type\":\"ARCHIVE\",\"resolver\":\"archivist\",\"name\":\"归档\"}"
|
||||
+ "]";
|
||||
|
||||
/** 检索文本里最多拼接的明细名称条数,避免超出 search_text 长度。 */
|
||||
private static final int SEARCH_ITEM_LIMIT = 10;
|
||||
|
||||
@Resource
|
||||
private ApprovalFacilityFormMapper facilityFormMapper;
|
||||
@Resource
|
||||
private ApprovalFacilityItemMapper facilityItemMapper;
|
||||
|
||||
@Override
|
||||
public String flowCode() {
|
||||
return ApprovalConstants.FLOW_FACILITY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String docType() {
|
||||
return ApprovalConstants.FLOW_FACILITY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String flowName() {
|
||||
return "设施损坏修复与租用价格表";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nodesJson() {
|
||||
return NODES_JSON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String noPrefix() {
|
||||
return ApprovalConstants.NO_PREFIX_FACILITY;
|
||||
}
|
||||
|
||||
/** 明细列:名称/位置/单位/数量/单价/金额(服务端重算)/备注。 */
|
||||
@Override
|
||||
public List<ApprovalLineSpec> lineColumns() {
|
||||
List<ApprovalLineSpec> columns = new ArrayList<>();
|
||||
columns.add(ApprovalLineSpec.text("itemName", "名称", true, 200));
|
||||
columns.add(ApprovalLineSpec.text("location", "位置", false, 200));
|
||||
columns.add(ApprovalLineSpec.text("unit", "单位", true, 20));
|
||||
columns.add(ApprovalLineSpec.decimal("quantity", "数量", true, 3));
|
||||
columns.add(ApprovalLineSpec.decimal("unitPrice", "单价", true, 2));
|
||||
// 金额由服务端重算,客户端不传也允许(客户端传了则必须与 数量 × 单价 一致)
|
||||
columns.add(ApprovalLineSpec.decimal("amount", "金额", false, 2).asMoney());
|
||||
columns.add(ApprovalLineSpec.text("remark", "备注", false, 200));
|
||||
return columns;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String requiredAttachmentCategory() {
|
||||
return ApprovalConstants.CATEGORY_SIGNED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ApprovalDocumentParam> paramClass() {
|
||||
return ApprovalFacilityParam.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* 草稿校验:生效日期必填且不早于今天;明细逐行校验并重算金额。
|
||||
*/
|
||||
@Override
|
||||
public void validate(ApprovalDocumentParam param, User user) {
|
||||
ApprovalFacilityParam facility = cast(param);
|
||||
requireEffectiveDate(facility.getEffectiveDate());
|
||||
ApprovalLineSupport.normalize(facility.getLines(), lineColumns());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long persist(Long instanceId, Long bizId, ApprovalDocumentParam param, User user) {
|
||||
ApprovalFacilityParam facility = cast(param);
|
||||
LocalDate effectiveDate = requireEffectiveDate(facility.getEffectiveDate());
|
||||
List<Map<String, Object>> rows = ApprovalLineSupport.normalize(facility.getLines(), lineColumns());
|
||||
|
||||
ApprovalFacilityForm form = bizId == null ? null : facilityFormMapper.selectById(bizId);
|
||||
if (form == null) {
|
||||
form = findByInstance(instanceId);
|
||||
}
|
||||
if (form == null) {
|
||||
form = new ApprovalFacilityForm();
|
||||
form.setInstanceId(instanceId);
|
||||
}
|
||||
form.setEffectiveDate(effectiveDate);
|
||||
if (form.getId() == null) {
|
||||
facilityFormMapper.insert(form);
|
||||
} else {
|
||||
facilityFormMapper.updateById(form);
|
||||
}
|
||||
|
||||
// 明细整表替换(ADR 0004):先删旧行再按提交顺序写入
|
||||
facilityItemMapper.delete(new LambdaQueryWrapper<ApprovalFacilityItem>()
|
||||
.eq(ApprovalFacilityItem::getInstanceId, instanceId));
|
||||
for (Map<String, Object> row : rows) {
|
||||
ApprovalFacilityItem item = new ApprovalFacilityItem();
|
||||
item.setInstanceId(instanceId);
|
||||
item.setLineNo((Integer) row.get(ApprovalLineSupport.KEY_LINE_NO));
|
||||
item.setItemName((String) row.get("itemName"));
|
||||
item.setLocation((String) row.get("location"));
|
||||
item.setUnit((String) row.get("unit"));
|
||||
item.setQuantity(decimal(row.get("quantity")));
|
||||
item.setUnitPrice(decimal(row.get("unitPrice")));
|
||||
item.setAmount(decimal(row.get(ApprovalLineSupport.KEY_AMOUNT)));
|
||||
item.setRemark((String) row.get("remark"));
|
||||
facilityItemMapper.insert(item);
|
||||
}
|
||||
return form.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load(ApprovalInstance instance, ApprovalDetail detail) {
|
||||
ApprovalFacilityForm form = findByInstance(instance.getId());
|
||||
if (form != null) {
|
||||
detail.addField("effectiveDate", "生效日期", format(form.getEffectiveDate()));
|
||||
}
|
||||
detail.setLines(lines(instance));
|
||||
detail.setLineColumns(lineColumns());
|
||||
com.gxwebsoft.approval.vo.ApprovalLineSummaryVO summary =
|
||||
new com.gxwebsoft.approval.vo.ApprovalLineSummaryVO();
|
||||
summary.setLineCount(lineCount(instance));
|
||||
summary.setAmountTotal(lineAmountTotal(instance));
|
||||
detail.setLineSummary(summary);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> lines(ApprovalInstance instance) {
|
||||
List<ApprovalFacilityItem> items = findItems(instance.getId());
|
||||
List<Map<String, Object>> rows = new ArrayList<>(items.size());
|
||||
for (ApprovalFacilityItem item : items) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
row.put(ApprovalLineSupport.KEY_LINE_NO, item.getLineNo());
|
||||
row.put("itemName", item.getItemName());
|
||||
row.put("location", item.getLocation());
|
||||
row.put("unit", item.getUnit());
|
||||
row.put("quantity", item.getQuantity());
|
||||
row.put("unitPrice", item.getUnitPrice());
|
||||
row.put(ApprovalLineSupport.KEY_AMOUNT, item.getAmount());
|
||||
row.put("remark", item.getRemark());
|
||||
rows.add(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigDecimal lineAmountTotal(ApprovalInstance instance) {
|
||||
List<ApprovalFacilityItem> items = findItems(instance.getId());
|
||||
if (items.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (ApprovalFacilityItem item : items) {
|
||||
if (item.getAmount() != null) {
|
||||
total = total.add(item.getAmount());
|
||||
}
|
||||
}
|
||||
return total.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readField(ApprovalInstance instance, String field) {
|
||||
if (!"effectiveDate".equals(field)) {
|
||||
return null;
|
||||
}
|
||||
ApprovalFacilityForm form = findByInstance(instance.getId());
|
||||
return form == null ? null : format(form.getEffectiveDate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String title(ApprovalInstance instance) {
|
||||
ApprovalFacilityForm form = findByInstance(instance.getId());
|
||||
if (form == null || form.getEffectiveDate() == null) {
|
||||
return "设施价格表";
|
||||
}
|
||||
return "设施价格表(生效 " + format(form.getEffectiveDate()) + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String searchText(ApprovalInstance instance) {
|
||||
String title = title(instance);
|
||||
List<ApprovalFacilityItem> items = findItems(instance.getId());
|
||||
if (items.isEmpty()) {
|
||||
return title;
|
||||
}
|
||||
String names = items.stream()
|
||||
.limit(SEARCH_ITEM_LIMIT)
|
||||
.map(ApprovalFacilityItem::getItemName)
|
||||
.filter(name -> name != null && !name.isEmpty())
|
||||
.collect(Collectors.joining(" "));
|
||||
return names.isEmpty() ? title : title + " " + names;
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档前校验(引擎在 {@code ApprovalTaskService.doArchive} 中调用):
|
||||
* 生效日期早于当天即拒绝,提示追溯修改不被允许。
|
||||
*/
|
||||
@Override
|
||||
public void validateBeforeArchive(ApprovalInstance instance) {
|
||||
ApprovalFacilityForm form = findByInstance(instance.getId());
|
||||
if (form == null || form.getEffectiveDate() == null) {
|
||||
throw new BusinessException("设施价格表缺少生效日期,不能归档");
|
||||
}
|
||||
if (form.getEffectiveDate().isBefore(LocalDate.now())) {
|
||||
throw new BusinessException("生效日期不能早于归档日,追溯修改不被允许");
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDate requireEffectiveDate(LocalDate effectiveDate) {
|
||||
if (effectiveDate == null) {
|
||||
throw new BusinessException("生效日期不能为空");
|
||||
}
|
||||
if (effectiveDate.isBefore(LocalDate.now())) {
|
||||
throw new BusinessException("生效日期不能早于今天,追溯修改不被允许");
|
||||
}
|
||||
return effectiveDate;
|
||||
}
|
||||
|
||||
private String format(LocalDate date) {
|
||||
return date == null ? null : date.toString();
|
||||
}
|
||||
|
||||
private BigDecimal decimal(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return value instanceof BigDecimal ? (BigDecimal) value : new BigDecimal(String.valueOf(value));
|
||||
}
|
||||
|
||||
private ApprovalFacilityForm findByInstance(Long instanceId) {
|
||||
List<ApprovalFacilityForm> forms = facilityFormMapper.selectList(
|
||||
new LambdaQueryWrapper<ApprovalFacilityForm>()
|
||||
.eq(ApprovalFacilityForm::getInstanceId, instanceId)
|
||||
.last("LIMIT 1"));
|
||||
return forms.isEmpty() ? null : forms.get(0);
|
||||
}
|
||||
|
||||
private List<ApprovalFacilityItem> findItems(Long instanceId) {
|
||||
if (instanceId == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return facilityItemMapper.selectList(new LambdaQueryWrapper<ApprovalFacilityItem>()
|
||||
.eq(ApprovalFacilityItem::getInstanceId, instanceId)
|
||||
.orderByAsc(ApprovalFacilityItem::getLineNo));
|
||||
}
|
||||
|
||||
private ApprovalFacilityParam cast(ApprovalDocumentParam param) {
|
||||
if (!(param instanceof ApprovalFacilityParam)) {
|
||||
throw new BusinessException("单据类型不匹配");
|
||||
}
|
||||
return (ApprovalFacilityParam) param;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.gxwebsoft.approval.doc.facility;
|
||||
|
||||
import com.gxwebsoft.approval.document.ApprovalLineSupport;
|
||||
import com.gxwebsoft.approval.document.ApprovalPriceProvider;
|
||||
import com.gxwebsoft.approval.document.ApprovalPriceVersion;
|
||||
import com.gxwebsoft.approval.entity.ApprovalFacilityItem;
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import com.gxwebsoft.approval.mapper.ApprovalFacilityPriceMapper;
|
||||
import com.gxwebsoft.approval.support.ApprovalTenantContext;
|
||||
import com.gxwebsoft.approval.vo.ApprovalFieldVO;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 设施价格表版本查询(ADR 0012):当前生效版本与历史版本。
|
||||
*
|
||||
* <p>审批实例即版本,不存在独立主数据表:
|
||||
* 当前生效版本 = 同租户同 flow_code 下已归档且生效日期不晚于今天的最新一版;
|
||||
* 历史版本 = 同条件按生效日期倒序。</p>
|
||||
*
|
||||
* <p>所有查询显式带 tenantId,不依赖请求头里的租户上下文——价格查看接口、员工端首页
|
||||
* 与内部调用都复用本服务。</p>
|
||||
*
|
||||
* <p>价格表只作参照,不参与其他单据的金额计算(ADR 0012),因此这里只读。</p>
|
||||
*/
|
||||
@Service
|
||||
public class FacilityPriceQueryService implements ApprovalPriceProvider {
|
||||
|
||||
/** 未指定条数时的历史上限。 */
|
||||
private static final int DEFAULT_HISTORY_LIMIT = 50;
|
||||
|
||||
@Resource
|
||||
private ApprovalFacilityPriceMapper priceMapper;
|
||||
@Resource
|
||||
private FacilityDocumentType facilityDocumentType;
|
||||
|
||||
/** 当前登录租户的当前生效版本;无已归档版本返回 null。 */
|
||||
@Override
|
||||
public ApprovalPriceVersion current() {
|
||||
return current(ApprovalTenantContext.currentTenantId());
|
||||
}
|
||||
|
||||
/** 指定租户的当前生效版本;无已归档版本返回 null。 */
|
||||
public ApprovalPriceVersion current(Integer tenantId) {
|
||||
if (tenantId == null) {
|
||||
return null;
|
||||
}
|
||||
List<FacilityPriceVersionRow> rows = priceMapper.selectArchivedVersions(
|
||||
tenantId, LocalDate.now(), 0, 1);
|
||||
return rows.isEmpty() ? null : toVersion(tenantId, rows.get(0));
|
||||
}
|
||||
|
||||
/** 当前登录租户的历史版本,按生效日期倒序。 */
|
||||
@Override
|
||||
public List<ApprovalPriceVersion> history(int limit) {
|
||||
return history(ApprovalTenantContext.currentTenantId(), 1, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定租户的历史版本分页查询,按生效日期倒序。
|
||||
*
|
||||
* @param page 页码,从 1 开始
|
||||
* @param limit 每页条数,≤0 时取默认值
|
||||
*/
|
||||
public List<ApprovalPriceVersion> history(Integer tenantId, int page, int limit) {
|
||||
if (tenantId == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
int size = limit <= 0 ? DEFAULT_HISTORY_LIMIT : limit;
|
||||
int pageNo = page <= 0 ? 1 : page;
|
||||
List<FacilityPriceVersionRow> rows = priceMapper.selectArchivedVersions(
|
||||
tenantId, LocalDate.now(), (pageNo - 1) * size, size);
|
||||
List<ApprovalPriceVersion> versions = new ArrayList<>(rows.size());
|
||||
for (FacilityPriceVersionRow row : rows) {
|
||||
ApprovalPriceVersion version = toVersion(tenantId, row);
|
||||
if (version != null) {
|
||||
versions.add(version);
|
||||
}
|
||||
}
|
||||
return versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 装配一个版本:实例定位字段 + 业务字段 + 明细 + 合计金额。
|
||||
*/
|
||||
private ApprovalPriceVersion toVersion(Integer tenantId, FacilityPriceVersionRow row) {
|
||||
ApprovalInstance instance = row.getInstanceId() == null ? null
|
||||
: priceMapper.selectInstance(tenantId, row.getInstanceId());
|
||||
if (instance == null) {
|
||||
return null;
|
||||
}
|
||||
ApprovalPriceVersion version = new ApprovalPriceVersion();
|
||||
version.setInstanceId(instance.getId());
|
||||
version.setBizNo(instance.getBizNo());
|
||||
version.setEffectiveDate(row.getEffectiveDate());
|
||||
version.setArchiveTime(row.getArchiveTime());
|
||||
version.setReportOrgName(row.getReportOrgName());
|
||||
|
||||
List<ApprovalFieldVO> fields = new ArrayList<>();
|
||||
fields.add(new ApprovalFieldVO("effectiveDate", "生效日期",
|
||||
row.getEffectiveDate() == null ? null : row.getEffectiveDate().toString()));
|
||||
version.setFields(fields);
|
||||
|
||||
List<Map<String, Object>> lines = new ArrayList<>();
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
boolean hasAmount = false;
|
||||
for (ApprovalFacilityItem item : priceMapper.selectItems(tenantId, row.getInstanceId())) {
|
||||
Map<String, Object> line = new LinkedHashMap<>();
|
||||
line.put(ApprovalLineSupport.KEY_LINE_NO, item.getLineNo());
|
||||
line.put("itemName", item.getItemName());
|
||||
line.put("location", item.getLocation());
|
||||
line.put("unit", item.getUnit());
|
||||
line.put("quantity", item.getQuantity());
|
||||
line.put("unitPrice", item.getUnitPrice());
|
||||
line.put(ApprovalLineSupport.KEY_AMOUNT, item.getAmount());
|
||||
line.put("remark", item.getRemark());
|
||||
lines.add(line);
|
||||
if (item.getAmount() != null) {
|
||||
hasAmount = true;
|
||||
total = total.add(item.getAmount());
|
||||
}
|
||||
}
|
||||
version.setLines(lines);
|
||||
version.setLineColumns(facilityDocumentType.lineColumns());
|
||||
version.setAmountTotal(hasAmount ? total.setScale(2, RoundingMode.HALF_UP) : null);
|
||||
return version;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.gxwebsoft.approval.doc.facility;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 已归档价格表版本行(版本列表查询结果,只含定位字段;明细按实例另取)。
|
||||
*/
|
||||
@Data
|
||||
public class FacilityPriceVersionRow implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long instanceId;
|
||||
|
||||
private String bizNo;
|
||||
|
||||
private LocalDate effectiveDate;
|
||||
|
||||
private LocalDateTime archiveTime;
|
||||
|
||||
private String reportOrgName;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.gxwebsoft.approval.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设施价格表(设施损坏修复与租用价格表):一个审批实例一行,明细见
|
||||
* {@link ApprovalFacilityItem}。
|
||||
*
|
||||
* <p>审批实例即版本(ADR 0012):本表不承载版本状态,「当前生效版本」由
|
||||
* 实例的归档状态与生效日期查询得出。</p>
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(description = "设施价格表")
|
||||
@TableName("approval_facility_form")
|
||||
public class ApprovalFacilityForm implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private Integer tenantId;
|
||||
|
||||
private Long instanceId;
|
||||
|
||||
@ApiModelProperty("生效日期,必填;不允许早于归档日(禁止追溯修改)")
|
||||
private LocalDate effectiveDate;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.gxwebsoft.approval.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设施价格表明细行:一版价格的条目(ADR 0012),保存时整表替换。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(description = "设施价格表明细")
|
||||
@TableName("approval_facility_item")
|
||||
public class ApprovalFacilityItem implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private Integer tenantId;
|
||||
|
||||
private Long instanceId;
|
||||
|
||||
@ApiModelProperty("行号,按提交顺序 1..N")
|
||||
private Integer lineNo;
|
||||
|
||||
@ApiModelProperty("名称")
|
||||
private String itemName;
|
||||
|
||||
@ApiModelProperty("位置")
|
||||
private String location;
|
||||
|
||||
@ApiModelProperty("单位")
|
||||
private String unit;
|
||||
|
||||
@ApiModelProperty("数量,3 位小数")
|
||||
private BigDecimal quantity;
|
||||
|
||||
@ApiModelProperty("单价,2 位小数")
|
||||
private BigDecimal unitPrice;
|
||||
|
||||
@ApiModelProperty("金额,服务端按数量 × 单价重算")
|
||||
private BigDecimal amount;
|
||||
|
||||
@ApiModelProperty("备注")
|
||||
private String remark;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.gxwebsoft.approval.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.gxwebsoft.approval.entity.ApprovalFacilityForm;
|
||||
|
||||
/**
|
||||
* 设施价格表 Mapper。
|
||||
*/
|
||||
public interface ApprovalFacilityFormMapper extends BaseMapper<ApprovalFacilityForm> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.gxwebsoft.approval.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.gxwebsoft.approval.entity.ApprovalFacilityItem;
|
||||
|
||||
/**
|
||||
* 设施价格表明细 Mapper。
|
||||
*/
|
||||
public interface ApprovalFacilityItemMapper extends BaseMapper<ApprovalFacilityItem> {
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.gxwebsoft.approval.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import com.gxwebsoft.approval.doc.facility.FacilityPriceVersionRow;
|
||||
import com.gxwebsoft.approval.entity.ApprovalFacilityItem;
|
||||
import com.gxwebsoft.approval.entity.ApprovalInstance;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 设施价格表版本查询(ADR 0012):实例状态与生效日期的联表。
|
||||
*
|
||||
* <p>显式传 tenantId 并跳过租户插件改写:价格表查看接口与测试都按指定租户查询,
|
||||
* 不依赖请求头里的租户上下文(服务也可能被定时任务或内部调用)。</p>
|
||||
*/
|
||||
public interface ApprovalFacilityPriceMapper {
|
||||
|
||||
/**
|
||||
* 已归档且生效日期不晚于今天的版本,按生效日期倒序、同日期按归档时间倒序。
|
||||
*
|
||||
* @param offset 起始偏移,从 0 开始
|
||||
* @param limit 最多返回条数;小于 0 表示不限
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
List<FacilityPriceVersionRow> selectArchivedVersions(@Param("tenantId") Integer tenantId,
|
||||
@Param("today") LocalDate today,
|
||||
@Param("offset") int offset,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/** 按实例id取实例(显式租户)。 */
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
ApprovalInstance selectInstance(@Param("tenantId") Integer tenantId,
|
||||
@Param("instanceId") Long instanceId);
|
||||
|
||||
/** 按实例id取明细行(显式租户,按行号升序)。 */
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
List<ApprovalFacilityItem> selectItems(@Param("tenantId") Integer tenantId,
|
||||
@Param("instanceId") Long instanceId);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.gxwebsoft.approval.mapper.ApprovalFacilityPriceMapper">
|
||||
|
||||
<!-- 当前生效版本 = 已归档且生效日期不晚于今天里 effective_date 最大的那版(ADR 0012) -->
|
||||
<select id="selectArchivedVersions" resultType="com.gxwebsoft.approval.doc.facility.FacilityPriceVersionRow">
|
||||
SELECT i.id AS instance_id,
|
||||
i.biz_no AS biz_no,
|
||||
f.effective_date AS effective_date,
|
||||
i.archive_time AS archive_time,
|
||||
i.report_org_name AS report_org_name
|
||||
FROM approval_instance i
|
||||
JOIN approval_facility_form f ON f.instance_id = i.id
|
||||
WHERE i.tenant_id = #{tenantId}
|
||||
AND i.flow_code = 'facility'
|
||||
AND i.status = 'ARCHIVED'
|
||||
AND f.effective_date <= #{today}
|
||||
ORDER BY f.effective_date DESC, i.archive_time DESC, i.id DESC
|
||||
<if test="limit >= 0">
|
||||
LIMIT #{limit} OFFSET #{offset}
|
||||
</if>
|
||||
</select>
|
||||
|
||||
<select id="selectInstance" resultType="com.gxwebsoft.approval.entity.ApprovalInstance">
|
||||
SELECT *
|
||||
FROM approval_instance
|
||||
WHERE id = #{instanceId} AND tenant_id = #{tenantId}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectItems" resultType="com.gxwebsoft.approval.entity.ApprovalFacilityItem">
|
||||
SELECT *
|
||||
FROM approval_facility_item
|
||||
WHERE instance_id = #{instanceId} AND tenant_id = #{tenantId}
|
||||
ORDER BY line_no
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.gxwebsoft.approval.param;
|
||||
|
||||
import com.gxwebsoft.approval.document.ApprovalDocumentParam;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 设施价格表草稿参数;id 为审批实例id,更新草稿时传入。
|
||||
*
|
||||
* <p>明细行在基类 {@link ApprovalDocumentParam#getLines()} 中按列 key 提交
|
||||
* (itemName/location/unit/quantity/unitPrice/amount/remark);金额由服务端重算。</p>
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ApprovalFacilityParam extends ApprovalDocumentParam {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("生效日期,必填,yyyy-MM-dd;允许未来生效,不允许早于今天")
|
||||
private LocalDate effectiveDate;
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
package com.gxwebsoft.approval;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.gxwebsoft.approval.doc.facility.FacilityPriceQueryService;
|
||||
import com.gxwebsoft.approval.document.ApprovalPriceVersion;
|
||||
import com.gxwebsoft.support.ApprovalTestData;
|
||||
import com.gxwebsoft.support.TestAuthentication;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* 设施价格表集成测试:四部门会签(含任一组驳回后全组重审)、双存档与归档、
|
||||
* 生效日期追溯拒绝、实例即版本(当前生效版本不因未来版本切换、历史版本倒序)、
|
||||
* 跨单据总表与明细导出的逐行数据。
|
||||
*/
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles({"gxtyzx-test", "test"})
|
||||
class ApprovalFacilityIT {
|
||||
|
||||
/**
|
||||
* 独立租户:部门负责人、总经理、录入员、收文员等角色按 (tenant_id, role_code) 解析,
|
||||
* 用专属租户避免与并行执行的其它审批测试类互相污染。
|
||||
*/
|
||||
private static final int TENANT = 10026;
|
||||
private static final String LIST = "approval:instance:list";
|
||||
private static final String SUBMIT = "approval:instance:submit";
|
||||
private static final String APPROVE = "approval:instance:approve";
|
||||
private static final String ARCHIVE = "approval:instance:archive";
|
||||
private static final String EXPORT = "approval:instance:export";
|
||||
private static final String FLOW = "facility";
|
||||
|
||||
/** 明细列顺序(与 FacilityDocumentType.lineColumns() 一致)。 */
|
||||
private static final String[] LINE_HEADERS = {
|
||||
"单号", "状态", "报送部门", "报送人", "名称", "位置", "单位", "数量", "单价", "金额", "备注"
|
||||
};
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private DataSource dataSource;
|
||||
@Autowired
|
||||
private FacilityPriceQueryService priceQueryService;
|
||||
|
||||
private ApprovalTestData data;
|
||||
private ApprovalTestData.TestUser reporter;
|
||||
private ApprovalTestData.TestUser reportLeader;
|
||||
private ApprovalTestData.TestUser deputy;
|
||||
private ApprovalTestData.TestUser generalManager;
|
||||
private ApprovalTestData.TestUser typist;
|
||||
private ApprovalTestData.TestUser archivist;
|
||||
private ApprovalTestData.TestUser venueLeader;
|
||||
private ApprovalTestData.TestUser opsLeader;
|
||||
private ApprovalTestData.TestUser safetyLeader;
|
||||
private ApprovalTestData.TestUser financeLeader;
|
||||
private Integer reportOrgId;
|
||||
/** 本用例直接插入的组织id(ApprovalTestData 不跟踪固编码组织)。 */
|
||||
private final List<Integer> fixedOrgIds = new ArrayList<>();
|
||||
/** 本用例产生的导出文件。 */
|
||||
private final List<Path> exportedFiles = new ArrayList<>();
|
||||
/** 本用例创建的实例id:价格表子表不在 ApprovalTestData 清理范围内,需自行删除。 */
|
||||
private final List<Long> instanceIds = new ArrayList<>();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
data = new ApprovalTestData(dataSource);
|
||||
String suffix = data.uniqueSuffix();
|
||||
reportOrgId = data.createOrganization("价格报送部门" + suffix, 136, TENANT);
|
||||
reporter = data.createUser("价格报送人" + suffix, reportOrgId, TENANT);
|
||||
|
||||
ApprovalTestData.TestRole reportLeaderRole =
|
||||
data.createRole(data.leaderRoleCode(reportOrgId), "价格报送部门负责人", TENANT);
|
||||
reportLeader = data.createUser("价格报送负责人" + suffix, reportOrgId, TENANT);
|
||||
data.bindTenant(reportLeader.userId, reportLeaderRole.roleId, TENANT);
|
||||
|
||||
ApprovalTestData.TestRole deputyRole =
|
||||
data.createRole(deputyRoleCode(reportOrgId), "价格报送部门分管领导", TENANT);
|
||||
deputy = data.createUser("价格分管领导" + suffix, reportOrgId, TENANT);
|
||||
data.bindTenant(deputy.userId, deputyRole.roleId, TENANT);
|
||||
|
||||
ApprovalTestData.TestRole gmRole =
|
||||
data.createRole("approval_general_manager", "价格总经理", TENANT);
|
||||
generalManager = data.createUser("价格总经理" + suffix, reportOrgId, TENANT);
|
||||
data.bindTenant(generalManager.userId, gmRole.roleId, TENANT);
|
||||
|
||||
ApprovalTestData.TestRole typistRole = data.createRole("approval_typist", "价格录入员", TENANT);
|
||||
typist = data.createUser("价格录入员" + suffix, reportOrgId, TENANT);
|
||||
data.bindTenant(typist.userId, typistRole.roleId, TENANT);
|
||||
|
||||
ApprovalTestData.TestRole archivistRole = data.createRole("approval_archivist", "价格收文员", TENANT);
|
||||
archivist = data.createUser("价格收文员" + suffix, reportOrgId, TENANT);
|
||||
data.bindTenant(archivist.userId, archivistRole.roleId, TENANT);
|
||||
|
||||
venueLeader = createOrgLeader("APPROVAL_VENUE", "场馆部", suffix);
|
||||
opsLeader = createOrgLeader("APPROVAL_OPS", "运营管理部", suffix);
|
||||
safetyLeader = createOrgLeader("APPROVAL_SAFETY", "安全运保部", suffix);
|
||||
financeLeader = createOrgLeader("APPROVAL_FINANCE", "财务部", suffix);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
for (Path path : exportedFiles) {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (Exception ignored) {
|
||||
// 文件清理失败不影响断言
|
||||
}
|
||||
}
|
||||
exportedFiles.clear();
|
||||
for (Long instanceId : instanceIds) {
|
||||
data.master().update("DELETE FROM approval_facility_item WHERE instance_id = ?", instanceId);
|
||||
data.master().update("DELETE FROM approval_facility_form WHERE instance_id = ?", instanceId);
|
||||
}
|
||||
instanceIds.clear();
|
||||
for (Integer organizationId : fixedOrgIds) {
|
||||
data.server().update("DELETE FROM sys_organization WHERE organization_id = ?", organizationId);
|
||||
}
|
||||
fixedOrgIds.clear();
|
||||
data.cleanup();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 用例
|
||||
|
||||
/**
|
||||
* 端到端:建草稿 → 逐行填价 → 提交 → 四部门会签(两组通过后第三组驳回 → round+1 →
|
||||
* 重提后四组全部重审)→ 分管领导 → 总经理 → 双存档 → 归档。
|
||||
*/
|
||||
@Test
|
||||
void runsEndToEndAndReopensAllCountersignGroupsAfterRejection() throws Exception {
|
||||
long instanceId = createDraft(LocalDate.now(), priceLines("JG端到端"));
|
||||
uploadSignedAttachment(instanceId);
|
||||
submit(instanceId);
|
||||
approve(reportLeader, pendingTask(instanceId, reportLeader.userId, "reportLeader"));
|
||||
|
||||
int countersignRound = roundOf(instanceId);
|
||||
assertThat(pendingGroupKeys(instanceId)).containsExactlyInAnyOrder("venue", "ops", "safety", "finance");
|
||||
|
||||
// 两组先通过:任务完成留痕,节点不推进
|
||||
approve(venueLeader, pendingTask(instanceId, venueLeader.userId, "deptCountersign"));
|
||||
approve(opsLeader, pendingTask(instanceId, opsLeader.userId, "deptCountersign"));
|
||||
assertThat(pendingGroupKeys(instanceId)).containsExactlyInAnyOrder("safety", "finance");
|
||||
assertThat(statusOf(instanceId)).isEqualTo("PENDING");
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND node_key = 'deptCountersign' AND round = ? AND status = 'DONE'", instanceId, countersignRound))
|
||||
.isEqualTo(2);
|
||||
|
||||
// 安全运保部驳回:整节点结束、round+1、已通过的组任务保持 DONE
|
||||
mockMvc.perform(authed(post("/api/approval/task/"
|
||||
+ pendingTask(instanceId, safetyLeader.userId, "deptCountersign") + "/reject"),
|
||||
safetyLeader, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"opinion\":\"单价依据不足,请补充\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("REJECTED"));
|
||||
|
||||
int rejectedRound = roundOf(instanceId);
|
||||
assertThat(rejectedRound).isGreaterThan(countersignRound);
|
||||
// 已通过的组任务保持 DONE 留痕(驳回者本人的任务也是 DONE,带驳回意见)
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND node_key = 'deptCountersign' AND round = ? AND status = 'DONE'"
|
||||
+ " AND group_key IN ('venue','ops')", instanceId, countersignRound))
|
||||
.isEqualTo(2);
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND node_key = 'deptCountersign' AND round = ? AND status = 'PENDING'", instanceId, countersignRound))
|
||||
.isZero();
|
||||
|
||||
// 重提:回到报送部门负责人,会签节点在新一轮重新生成四组待办
|
||||
resubmit(instanceId);
|
||||
approve(reportLeader, pendingTask(instanceId, reportLeader.userId, "reportLeader"));
|
||||
int reopenedRound = roundOf(instanceId);
|
||||
assertThat(reopenedRound).isGreaterThan(rejectedRound);
|
||||
assertThat(pendingGroupKeys(instanceId)).containsExactlyInAnyOrder("venue", "ops", "safety", "finance");
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND node_key = 'deptCountersign' AND round = ?", instanceId, reopenedRound)).isEqualTo(4);
|
||||
|
||||
approve(venueLeader, pendingTask(instanceId, venueLeader.userId, "deptCountersign"));
|
||||
approve(opsLeader, pendingTask(instanceId, opsLeader.userId, "deptCountersign"));
|
||||
approve(safetyLeader, pendingTask(instanceId, safetyLeader.userId, "deptCountersign"));
|
||||
assertThat(statusOf(instanceId)).isEqualTo("PENDING");
|
||||
approve(financeLeader, pendingTask(instanceId, financeLeader.userId, "deptCountersign"));
|
||||
|
||||
// 分管领导 → 总经理
|
||||
approve(deputy, pendingTask(instanceId, deputy.userId, "deputyLeader"));
|
||||
approve(generalManager, pendingTask(instanceId, generalManager.userId, "generalManager"));
|
||||
|
||||
// 双存档:报送人 + 运营管理部录入员
|
||||
mockMvc.perform(authed(post("/api/approval/task/"
|
||||
+ pendingTask(instanceId, reporter.userId, "confirm") + "/confirm"), reporter, APPROVE))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("PENDING"));
|
||||
mockMvc.perform(authed(post("/api/approval/task/"
|
||||
+ pendingTask(instanceId, typist.userId, "confirm") + "/confirm"), typist, APPROVE))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("APPROVED"));
|
||||
|
||||
String archived = mockMvc.perform(authed(post("/api/approval/task/"
|
||||
+ pendingTask(instanceId, archivist.userId, "archive") + "/archive"), archivist, ARCHIVE))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("ARCHIVED"))
|
||||
.andExpect(jsonPath("$.data.archiveTime").isNotEmpty())
|
||||
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(objectMapper.readTree(archived).path("data").path("bizNo").asText()).startsWith("JG");
|
||||
|
||||
// 详情:明细逐行、合计金额与四组会签进度
|
||||
mockMvc.perform(authed(
|
||||
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get(
|
||||
"/api/approval/instance/" + instanceId), reporter, LIST))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.document.fields[0].key").value("effectiveDate"))
|
||||
.andExpect(jsonPath("$.data.document.lines.length()").value(3))
|
||||
.andExpect(jsonPath("$.data.document.lines[0].itemName").value("JG端到端-1"))
|
||||
.andExpect(jsonPath("$.data.document.lines[0].location").value("羽毛球馆"))
|
||||
.andExpect(jsonPath("$.data.document.lines[0].unit").value("个"))
|
||||
.andExpect(jsonPath("$.data.document.lines[0].quantity").isNumber())
|
||||
.andExpect(jsonPath("$.data.document.lines[0].unitPrice").isNumber())
|
||||
.andExpect(jsonPath("$.data.document.lines[0].amount").isNumber())
|
||||
.andExpect(jsonPath("$.data.document.lines[0].remark").value("端到端备注"))
|
||||
.andExpect(jsonPath("$.data.document.lineSummary.lineCount").value(3))
|
||||
.andExpect(jsonPath("$.data.document.lineColumns.length()").value(7))
|
||||
// 会签节点 4 组 + 存档节点 2 组
|
||||
.andExpect(jsonPath("$.data.document.groupProgress.length()").value(6))
|
||||
.andExpect(jsonPath("$.data.document.groupProgress[0].groupKey").value("venue"))
|
||||
.andExpect(jsonPath("$.data.document.groupProgress[0].signed").value(1))
|
||||
.andExpect(jsonPath("$.data.document.groupProgress[0].total").value(1))
|
||||
.andExpect(jsonPath("$.data.document.groupProgress[3].groupKey").value("finance"));
|
||||
|
||||
// 明细行落库:金额由服务端重算
|
||||
assertThat(data.queryMaster("SELECT amount FROM approval_facility_item WHERE instance_id = ? "
|
||||
+ "AND line_no = 2", BigDecimal.class, instanceId)).isEqualByComparingTo("125.00");
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_facility_item WHERE instance_id = ?",
|
||||
instanceId)).isEqualTo(3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生效日期早于当天:草稿保存即拒绝;已进入审批的单据若被改成过去日期,归档时拒绝。
|
||||
*/
|
||||
@Test
|
||||
void rejectsRetroactiveEffectiveDateOnSaveAndOnArchive() throws Exception {
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + FLOW), reporter, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body(LocalDate.now().minusDays(1), priceLines("JG追溯"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("追溯修改不被允许")));
|
||||
|
||||
long instanceId = createDraft(LocalDate.now(), priceLines("JG归档追溯"));
|
||||
uploadSignedAttachment(instanceId);
|
||||
submit(instanceId);
|
||||
runToApproved(instanceId);
|
||||
|
||||
// 走完审批后把生效日期改到昨天(模拟追溯修改),归档必须被拦下
|
||||
data.master().update("UPDATE approval_facility_form SET effective_date = ? WHERE instance_id = ?",
|
||||
LocalDate.now().minusDays(1), instanceId);
|
||||
mockMvc.perform(authed(post("/api/approval/task/"
|
||||
+ pendingTask(instanceId, archivist.userId, "archive") + "/archive"), archivist, ARCHIVE))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(1))
|
||||
.andExpect(jsonPath("$.message").value(org.hamcrest.Matchers.containsString("追溯修改不被允许")));
|
||||
|
||||
assertThat(statusOf(instanceId)).isEqualTo("APPROVED");
|
||||
assertThat(countPendingTasks(instanceId, "archive")).isEqualTo(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实例即版本:归档一版今天生效的价格后,再归档一版未来生效的,当前生效版本不切换。
|
||||
*/
|
||||
@Test
|
||||
void doesNotSwitchCurrentVersionWhenFutureVersionIsArchived() throws Exception {
|
||||
long current = archiveVersion(LocalDate.now(), "JG当前版", "120.00");
|
||||
|
||||
ApprovalPriceVersion before = priceQueryService.current(TENANT);
|
||||
assertThat(before).as("归档后应有当前生效版本").isNotNull();
|
||||
assertThat(before.getInstanceId()).isEqualTo(current);
|
||||
assertThat(before.getEffectiveDate()).isEqualTo(LocalDate.now());
|
||||
assertThat(before.getAmountTotal()).isEqualByComparingTo("120.00");
|
||||
assertThat(before.getLines()).hasSize(1);
|
||||
assertThat(before.getLineColumns()).hasSize(7);
|
||||
|
||||
long future = archiveVersion(LocalDate.now().plusDays(10), "JG未来版", "999.00");
|
||||
assertThat(statusOf(future)).isEqualTo("ARCHIVED");
|
||||
|
||||
ApprovalPriceVersion after = priceQueryService.current(TENANT);
|
||||
assertThat(after).as("未来生效版本归档不改变当前生效版本").isNotNull();
|
||||
assertThat(after.getInstanceId()).isEqualTo(current);
|
||||
assertThat(after.getAmountTotal()).isEqualByComparingTo("120.00");
|
||||
|
||||
// 历史版本只含已生效的版本
|
||||
assertThat(priceQueryService.history(TENANT, 1, 10))
|
||||
.extracting(ApprovalPriceVersion::getInstanceId)
|
||||
.contains(current)
|
||||
.doesNotContain(future);
|
||||
}
|
||||
|
||||
/** 历史版本按生效日期倒序。 */
|
||||
@Test
|
||||
void ordersHistoryByEffectiveDateDescending() throws Exception {
|
||||
long older = archiveVersion(LocalDate.now(), "JG旧版", "100.00");
|
||||
long newer = archiveVersion(LocalDate.now(), "JG新版", "200.00");
|
||||
// 两版都按「今天生效」归档(归档校验要求不早于今天),再回填不同生效日期验证排序
|
||||
data.master().update("UPDATE approval_facility_form SET effective_date = ? WHERE instance_id = ?",
|
||||
LocalDate.now().minusDays(30), older);
|
||||
data.master().update("UPDATE approval_facility_form SET effective_date = ? WHERE instance_id = ?",
|
||||
LocalDate.now().minusDays(3), newer);
|
||||
|
||||
List<ApprovalPriceVersion> history = priceQueryService.history(TENANT, 1, 10);
|
||||
assertThat(history).extracting(ApprovalPriceVersion::getInstanceId)
|
||||
.containsSequence(newer, older);
|
||||
assertThat(history.get(0).getEffectiveDate()).isAfter(history.get(1).getEffectiveDate());
|
||||
assertThat(priceQueryService.current(TENANT).getInstanceId()).isEqualTo(newer);
|
||||
}
|
||||
|
||||
/** 明细导出含逐行数据;跨单据总表填「明细行数」「合计金额」。 */
|
||||
@Test
|
||||
void exportsFacilityLinesAndSummaryColumns() throws Exception {
|
||||
long instanceId = createDraft(LocalDate.now(), priceLines("JG导出"));
|
||||
uploadSignedAttachment(instanceId);
|
||||
submit(instanceId);
|
||||
runToApproved(instanceId);
|
||||
archive(instanceId);
|
||||
|
||||
String response = mockMvc.perform(authed(post("/api/approval/export/" + FLOW + "/lines"),
|
||||
archivist, EXPORT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"reporterId\":" + reporter.userId + "}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||
File linesFile = exportFile(response, "excel/审批单据明细-" + FLOW + "-");
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(linesFile))) {
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
assertThat(sheet.getSheetName()).isEqualTo("设施损坏修复与租用价格表");
|
||||
List<String> headers = new ArrayList<>();
|
||||
for (int i = 0; i < LINE_HEADERS.length; i++) {
|
||||
headers.add(cellText(sheet.getRow(0).getCell(i)));
|
||||
}
|
||||
assertThat(headers).containsExactly(LINE_HEADERS);
|
||||
assertThat(sheet.getLastRowNum() + 1).isEqualTo(4);
|
||||
Row first = sheet.getRow(1);
|
||||
assertThat(cellText(first.getCell(1))).isEqualTo("已归档");
|
||||
assertThat(cellText(first.getCell(4))).isEqualTo("JG导出-1");
|
||||
assertThat(cellText(first.getCell(6))).isEqualTo("个");
|
||||
assertThat(cellText(first.getCell(7))).isEqualTo("2.5");
|
||||
assertThat(cellText(first.getCell(8))).isEqualTo("100");
|
||||
assertThat(cellText(first.getCell(9))).isEqualTo("250");
|
||||
assertThat(cellText(sheet.getRow(3).getCell(4))).isEqualTo("JG导出-3");
|
||||
}
|
||||
|
||||
String summaryResponse = mockMvc.perform(authed(post("/api/approval/export"), archivist, EXPORT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"reporterId\":" + reporter.userId + ",\"flowCode\":\"" + FLOW + "\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||
File summaryFile = exportFile(summaryResponse, "excel/审批单据-");
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(summaryFile))) {
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
assertThat(sheet.getLastRowNum()).isEqualTo(1);
|
||||
Row row = sheet.getRow(1);
|
||||
assertThat(cellText(row.getCell(0))).startsWith("JG");
|
||||
assertThat(cellText(row.getCell(1))).isEqualTo("设施损坏修复与租用价格表");
|
||||
assertThat(cellText(row.getCell(16))).isEqualTo("3");
|
||||
assertThat(cellText(row.getCell(17))).isEqualTo("375.00");
|
||||
assertThat(cellText(row.getCell(18))).contains("四部门会签 已签 4/4");
|
||||
}
|
||||
assertThat(data.countMaster("SELECT COUNT(*) FROM approval_facility_item WHERE instance_id = ?",
|
||||
instanceId)).isEqualTo(3);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 流程辅助
|
||||
|
||||
private long archiveVersion(LocalDate effectiveDate, String itemName, String amount) throws Exception {
|
||||
// 单行版本:数量 1 × 单价 amount,合计即 amount
|
||||
List<Map<String, Object>> lines = new ArrayList<>();
|
||||
lines.add(line(itemName, null, "项", "1", amount, null));
|
||||
long instanceId = createDraft(effectiveDate, lines);
|
||||
uploadSignedAttachment(instanceId);
|
||||
submit(instanceId);
|
||||
runToApproved(instanceId);
|
||||
archive(instanceId);
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
/** 提交后的完整审批链(不含建草稿与提交)。 */
|
||||
private void runToApproved(long instanceId) throws Exception {
|
||||
approve(reportLeader, pendingTask(instanceId, reportLeader.userId, "reportLeader"));
|
||||
approve(venueLeader, pendingTask(instanceId, venueLeader.userId, "deptCountersign"));
|
||||
approve(opsLeader, pendingTask(instanceId, opsLeader.userId, "deptCountersign"));
|
||||
approve(safetyLeader, pendingTask(instanceId, safetyLeader.userId, "deptCountersign"));
|
||||
approve(financeLeader, pendingTask(instanceId, financeLeader.userId, "deptCountersign"));
|
||||
approve(deputy, pendingTask(instanceId, deputy.userId, "deputyLeader"));
|
||||
approve(generalManager, pendingTask(instanceId, generalManager.userId, "generalManager"));
|
||||
mockMvc.perform(authed(post("/api/approval/task/"
|
||||
+ pendingTask(instanceId, reporter.userId, "confirm") + "/confirm"), reporter, APPROVE))
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
mockMvc.perform(authed(post("/api/approval/task/"
|
||||
+ pendingTask(instanceId, typist.userId, "confirm") + "/confirm"), typist, APPROVE))
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("APPROVED"));
|
||||
}
|
||||
|
||||
private void archive(long instanceId) throws Exception {
|
||||
mockMvc.perform(authed(post("/api/approval/task/"
|
||||
+ pendingTask(instanceId, archivist.userId, "archive") + "/archive"), archivist, ARCHIVE))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.status").value("ARCHIVED"));
|
||||
}
|
||||
|
||||
private long createDraft(LocalDate effectiveDate, List<Map<String, Object>> lines) throws Exception {
|
||||
String response = mockMvc.perform(authed(post("/api/approval/instance/" + FLOW), reporter, SUBMIT)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body(effectiveDate, lines)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||
long instanceId = objectMapper.readTree(response).path("data").asLong();
|
||||
data.trackInstance(instanceId);
|
||||
instanceIds.add(instanceId);
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
private void submit(long instanceId) throws Exception {
|
||||
mockMvc.perform(authed(post("/api/approval/instance/" + instanceId + "/submit"), reporter, SUBMIT))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
}
|
||||
|
||||
private void resubmit(long instanceId) throws Exception {
|
||||
submit(instanceId);
|
||||
}
|
||||
|
||||
private void approve(ApprovalTestData.TestUser user, long taskId) throws Exception {
|
||||
String response = mockMvc.perform(authed(post("/api/approval/task/" + taskId + "/approve"), user, APPROVE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"opinion\":\"同意\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(objectMapper.readTree(response).path("code").asInt())
|
||||
.as("通过应成功:" + response).isZero();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 数据辅助
|
||||
|
||||
/** 三行明细:数量 × 单价 由服务端重算,合计 375.00。 */
|
||||
private List<Map<String, Object>> priceLines(String prefix) {
|
||||
List<Map<String, Object>> lines = new ArrayList<>();
|
||||
lines.add(line(prefix + "-1", "羽毛球馆", "个", "2.5", "100", "端到端备注"));
|
||||
lines.add(line(prefix + "-2", "游泳馆", "块", "1", "125", null));
|
||||
lines.add(line(prefix + "-3", null, "小时", "0", "80", null));
|
||||
return lines;
|
||||
}
|
||||
|
||||
private Map<String, Object> line(String itemName, String location, String unit,
|
||||
String quantity, String unitPrice, String remark) {
|
||||
Map<String, Object> line = new LinkedHashMap<>();
|
||||
line.put("itemName", itemName);
|
||||
if (location != null) {
|
||||
line.put("location", location);
|
||||
}
|
||||
line.put("unit", unit);
|
||||
line.put("quantity", quantity);
|
||||
line.put("unitPrice", unitPrice);
|
||||
if (remark != null) {
|
||||
line.put("remark", remark);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
private String body(LocalDate effectiveDate, List<Map<String, Object>> lines) throws Exception {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("effectiveDate", effectiveDate.toString());
|
||||
body.put("lines", lines);
|
||||
return objectMapper.writeValueAsString(body);
|
||||
}
|
||||
|
||||
/**
|
||||
* 会签组按固定组织编码解析(APPROVAL_VENUE 等),编码在每个租户内应唯一:
|
||||
* 已存在就复用(不删除),否则新建并在 teardown 删除,避免共享库里堆积同名编码。
|
||||
*/
|
||||
private ApprovalTestData.TestUser createOrgLeader(String code, String name, String suffix) {
|
||||
Integer organizationId = existingOrganization(code);
|
||||
if (organizationId == null) {
|
||||
data.server().update("INSERT INTO sys_organization (parent_id, organization_name, "
|
||||
+ "organization_full_name, organization_code, organization_type, park, phone, email, "
|
||||
+ "business, industry, image, about, sort_number, tenant_id, deleted) "
|
||||
+ "VALUES (136, ?, ?, ?, '部门', '', '', '', '', '', '', '', 100, ?, 0)",
|
||||
name, name, code, TENANT);
|
||||
organizationId = data.server().queryForObject(
|
||||
"SELECT organization_id FROM sys_organization WHERE tenant_id = ? AND organization_code = ? "
|
||||
+ "ORDER BY organization_id DESC LIMIT 1", Integer.class, TENANT, code);
|
||||
fixedOrgIds.add(organizationId);
|
||||
}
|
||||
|
||||
String roleCode = "dept_leader_" + code;
|
||||
Integer leaderRoleId = existingRoleId(roleCode);
|
||||
if (leaderRoleId == null) {
|
||||
leaderRoleId = data.createRole(roleCode, name + "负责人", TENANT).roleId;
|
||||
}
|
||||
ApprovalTestData.TestUser leader = data.createUser(name + "负责人" + suffix, organizationId, TENANT);
|
||||
data.bindTenant(leader.userId, leaderRoleId, TENANT);
|
||||
return leader;
|
||||
}
|
||||
|
||||
private Integer existingOrganization(String code) {
|
||||
List<Integer> ids = data.server().queryForList(
|
||||
"SELECT organization_id FROM sys_organization WHERE tenant_id = ? AND organization_code = ? "
|
||||
+ "AND deleted = 0 ORDER BY organization_id LIMIT 1", Integer.class, TENANT, code);
|
||||
return ids.isEmpty() ? null : ids.get(0);
|
||||
}
|
||||
|
||||
private Integer existingRoleId(String roleCode) {
|
||||
List<Integer> ids = data.server().queryForList(
|
||||
"SELECT role_id FROM sys_role WHERE tenant_id = ? AND role_code = ? AND deleted = 0 "
|
||||
+ "ORDER BY role_id LIMIT 1", Integer.class, TENANT, roleCode);
|
||||
return ids.isEmpty() ? null : ids.get(0);
|
||||
}
|
||||
|
||||
private String deputyRoleCode(Integer organizationId) {
|
||||
return "dept_deputy_" + data.organizationCode(organizationId);
|
||||
}
|
||||
|
||||
private long pendingTask(long instanceId, int userId, String nodeKey) {
|
||||
Long taskId = data.queryMaster("SELECT id FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND assignee_id = ? AND node_key = ? AND status = 'PENDING' ORDER BY id DESC LIMIT 1",
|
||||
Long.class, instanceId, userId, nodeKey);
|
||||
assertThat(taskId).as("节点 " + nodeKey + " 应有待办").isNotNull();
|
||||
return taskId;
|
||||
}
|
||||
|
||||
private List<String> pendingGroupKeys(long instanceId) {
|
||||
int round = roundOf(instanceId);
|
||||
return data.master().queryForList("SELECT group_key FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND node_key = 'deptCountersign' AND round = ? AND status = 'PENDING' ORDER BY group_seq",
|
||||
String.class, instanceId, round);
|
||||
}
|
||||
|
||||
private int roundOf(long instanceId) {
|
||||
return data.queryMaster("SELECT round FROM approval_instance WHERE id = ?", Integer.class, instanceId);
|
||||
}
|
||||
|
||||
private String statusOf(long instanceId) {
|
||||
return data.queryMaster("SELECT status FROM approval_instance WHERE id = ?", String.class, instanceId);
|
||||
}
|
||||
|
||||
private int countPendingTasks(long instanceId, String nodeKey) {
|
||||
return data.countMaster("SELECT COUNT(*) FROM approval_task WHERE instance_id = ? "
|
||||
+ "AND node_key = ? AND status = 'PENDING'", instanceId, nodeKey);
|
||||
}
|
||||
|
||||
private void uploadSignedAttachment(long instanceId) throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile("file", "sign.png", "image/png",
|
||||
new byte[]{(byte) 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A});
|
||||
mockMvc.perform(multipart("/api/approval/attachment/upload")
|
||||
.file(file)
|
||||
.param("instanceId", String.valueOf(instanceId))
|
||||
.param("category", "SIGNED")
|
||||
.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(reporter.userId, reporter.username,
|
||||
reportOrgId, TENANT, SUBMIT))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0));
|
||||
}
|
||||
|
||||
private File exportFile(String response, String namePrefix) throws Exception {
|
||||
var node = objectMapper.readTree(response).path("data");
|
||||
assertThat(node.path("name").asText()).startsWith(namePrefix);
|
||||
File file = new File(node.path("path").asText());
|
||||
assertThat(file).exists();
|
||||
exportedFiles.add(file.toPath());
|
||||
return file;
|
||||
}
|
||||
|
||||
private String cellText(org.apache.poi.ss.usermodel.Cell cell) {
|
||||
if (cell == null) {
|
||||
return "";
|
||||
}
|
||||
if (cell.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC) {
|
||||
BigDecimal value = BigDecimal.valueOf(cell.getNumericCellValue()).stripTrailingZeros();
|
||||
return value.toPlainString();
|
||||
}
|
||||
return cell.getStringCellValue();
|
||||
}
|
||||
|
||||
private MockHttpServletRequestBuilder authed(MockHttpServletRequestBuilder builder,
|
||||
ApprovalTestData.TestUser user,
|
||||
String... authorities) {
|
||||
return builder.header("tenantId", TENANT)
|
||||
.with(authentication(TestAuthentication.user(user.userId, user.username, reportOrgId,
|
||||
TENANT, authorities)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user