feat(hjc): 已支付订单下发标书附件,并堵住项目接口的标书地址泄露
「购买后才能下载标书」此前在服务端从未成立:`GET /api/hjc/bid-project/{id}` 与 `/list`
匿名即可拿到 `tenderFile`,订单详情也会回带 `project.tenderFile`;而订单列表接口
(`/hjc/order/my`)根本不带附件,PC 端连下载入口都没有。
- 新增 `HjcTenderFileUtil` + `HjcTenderFileVo`:把 `tender_file`(一站式推的 `files`:
JSON 字符串数组 / 对象数组 / 逗号分隔)解析成 `{name,url}`;坏数据只丢弃、不抛异常
- `HjcOrder` 加非表字段 `tenderFiles`;`/hjc/order/my` 与 `/hjc/order/{id}` **只有
`payStatus == 1`** 才附带(列表整批一次 `listByIds`,不 N+1;其余订单保持 null)
- 订单详情的 `project.tenderFile` 恒置 null:订单路径不留第二条地址来源
- `/hjc/bid-project/list` 一律不下发 `tenderFile`;`/hjc/bid-project/{id}` 仅 hjc 管理员
(后台编辑表单要回填)或已为本项目付过款的买家可见,其余置 null,且**不返回 401**
(详情对未登录是正常可用页面)
- 单测:`HjcTenderFileUtilTest`(12)、`HjcOrderControllerTenderFilesTest`(8)、
`HjcBidProjectControllerDetailTest` 扩展 5 条(含既有 favorited/buyerCount 不回归)
→ `com.gxwebsoft.hjc.**` 共 147 条全通过
规格与验收:`.scratch/hjc-tender-download/`;两端前端同批改动见 hjc-web 2e847b5、hjc-h5 5412485
This commit is contained in:
@@ -3,6 +3,7 @@ package com.gxwebsoft.hjc.controller;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.core.web.PageResult;
|
||||
import com.gxwebsoft.common.core.web.BaseController;
|
||||
import com.gxwebsoft.hjc.auth.HjcAdminGuard;
|
||||
import com.gxwebsoft.hjc.entity.HjcBidProject;
|
||||
import com.gxwebsoft.hjc.entity.HjcEnterprise;
|
||||
import com.gxwebsoft.hjc.param.HjcBidProjectParam;
|
||||
@@ -16,9 +17,16 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 汇吉采标书项目(C端浏览 + 管理后台维护)
|
||||
*
|
||||
* <p><b>C 端接口不下发标书附件的下载地址</b>({@code tenderFile}):它是付费内容,
|
||||
* 只在订单接口上、且订单已支付时才给(见 {@code HjcOrderController#attachTenderFiles})。
|
||||
* 本类里 {@code list} 恒置 null、{@code detail} 只对「管理员」或「已为本项目付过款的买家」放行。
|
||||
* 不做这一步的话,UI 上加多少门槛都只是装饰——匿名 curl 一次就绕过了。</p>
|
||||
*/
|
||||
@Tag(name = "汇吉采-标书项目")
|
||||
@RestController
|
||||
@@ -33,11 +41,14 @@ public class HjcBidProjectController extends BaseController {
|
||||
private HjcEnterpriseService hjcEnterpriseService;
|
||||
@Resource
|
||||
private HjcProjectFavoriteService hjcProjectFavoriteService;
|
||||
@Resource
|
||||
private HjcAdminGuard hjcGuard;
|
||||
|
||||
@Operation(summary = "分页查询(后台)")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@hjcGuard.isAdmin()")
|
||||
public ApiResult<PageResult<HjcBidProject>> page(HjcBidProjectParam param) {
|
||||
// 后台**保留** tenderFile:hjc-vue 的编辑表单要靠它回填
|
||||
return success(hjcBidProjectService.pageRel(param));
|
||||
}
|
||||
|
||||
@@ -45,7 +56,17 @@ public class HjcBidProjectController extends BaseController {
|
||||
@GetMapping("/list")
|
||||
public ApiResult<PageResult<HjcBidProject>> list(HjcBidProjectParam param) {
|
||||
param.setStatus(1);
|
||||
return success(hjcBidProjectService.pageRel(param));
|
||||
PageResult<HjcBidProject> page = hjcBidProjectService.pageRel(param);
|
||||
// C 端列表从不渲染附件,而这里是**匿名可读**的:一律不下发地址(后台走 /page,不受影响)
|
||||
List<HjcBidProject> list = page.getList();
|
||||
if (list != null) {
|
||||
for (HjcBidProject project : list) {
|
||||
if (project != null) {
|
||||
project.setTenderFile(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
return success(page);
|
||||
}
|
||||
|
||||
@Operation(summary = "详情")
|
||||
@@ -63,16 +84,39 @@ public class HjcBidProjectController extends BaseController {
|
||||
// 未登录 / 未注册企业一律 false —— 详情是公开接口,**不能**在这里返回 401,
|
||||
// 否则会把「收藏态查不到」升级成「整个详情页看不了」。
|
||||
project.setFavorited(false);
|
||||
// 登录态只解析一次企业,收藏态与「是否已购买」共用(别查两遍)
|
||||
HjcEnterprise enterprise = null;
|
||||
Integer userId = getLoginUserId();
|
||||
if (userId != null) {
|
||||
HjcEnterprise enterprise = hjcEnterpriseService.getByUserId(userId);
|
||||
enterprise = hjcEnterpriseService.getByUserId(userId);
|
||||
if (enterprise != null) {
|
||||
project.setFavorited(hjcProjectFavoriteService.exists(enterprise.getId(), project.getId()));
|
||||
}
|
||||
}
|
||||
// 附件下载地址是付费内容:管理员(后台编辑表单要回填)或已为本项目付过款的买家才拿到,
|
||||
// 其余(含匿名)一律抹掉。判定口径复用订单侧既有的 pay_status = 1,不新造一套。
|
||||
if (!isTenderFileVisible(project.getId(), enterprise)) {
|
||||
project.setTenderFile(null);
|
||||
}
|
||||
return success(project);
|
||||
}
|
||||
|
||||
/**
|
||||
* 本次请求是否有权看到该项目的标书附件地址。
|
||||
*
|
||||
* <p>两条放行路径:① hjc 管理员;② 登录买家且其企业在本项目上有已付款订单。</p>
|
||||
*/
|
||||
private boolean isTenderFileVisible(Integer projectId, HjcEnterprise enterprise) {
|
||||
if (hjcGuard.isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
if (enterprise == null) {
|
||||
return false;
|
||||
}
|
||||
return !hjcOrderService.listPaidProjectIds(enterprise.getId(), Collections.singletonList(projectId))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Operation(summary = "新增(后台)")
|
||||
@PostMapping()
|
||||
@PreAuthorize("@hjcGuard.isAdmin()")
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.gxwebsoft.hjc.service.HjcBizService;
|
||||
import com.gxwebsoft.hjc.service.HjcEnterpriseService;
|
||||
import com.gxwebsoft.hjc.service.HjcOrderService;
|
||||
import com.gxwebsoft.hjc.util.HjcOrderCancelUtil;
|
||||
import com.gxwebsoft.hjc.util.HjcTenderFileUtil;
|
||||
import com.gxwebsoft.payment.dto.PaymentRequest;
|
||||
import com.gxwebsoft.payment.dto.PaymentResponse;
|
||||
import com.gxwebsoft.payment.enums.PaymentStatus;
|
||||
@@ -34,6 +35,7 @@ import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -278,6 +280,8 @@ public class HjcOrderController extends BaseController {
|
||||
List<HjcOrder> list = hjcOrderService.list(new LambdaQueryWrapper<HjcOrder>()
|
||||
.eq(HjcOrder::getEnterpriseId, enterprise.getId())
|
||||
.orderByDesc(HjcOrder::getId));
|
||||
// 已支付订单带上标书附件(其余订单 tenderFiles 保持 null)。整批一次查项目,不要逐单查。
|
||||
attachTenderFiles(list);
|
||||
return success(list);
|
||||
}
|
||||
|
||||
@@ -300,7 +304,14 @@ public class HjcOrderController extends BaseController {
|
||||
if (denied != null) {
|
||||
return denied;
|
||||
}
|
||||
order.setProject(hjcBidProjectService.getById(order.getProjectId()));
|
||||
HjcBidProject project = hjcBidProjectService.getById(order.getProjectId());
|
||||
order.setProject(project);
|
||||
// 顺序不能倒:先从 project.tenderFile 解析出附件(只有已支付订单会拿到),
|
||||
// 再把项目上的原始字段抹掉——订单路径不提供第二条地址来源(见 HjcOrder#tenderFiles)。
|
||||
attachTenderFiles(order);
|
||||
if (project != null) {
|
||||
project.setTenderFile(null);
|
||||
}
|
||||
return success(order);
|
||||
}
|
||||
|
||||
@@ -340,6 +351,71 @@ public class HjcOrderController extends BaseController {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给**批量**订单附带标书附件(「我的订单」列表用)。
|
||||
*
|
||||
* <p>只有 {@code payStatus == 1}(已支付)的订单会拿到;其余订单的 {@code tenderFiles} 保持
|
||||
* {@code null}——「没付费」与「付费了但项目没附件」(空列表)是两件事,前端据此区分
|
||||
* 「不给下载」与「暂无附件」。</p>
|
||||
*
|
||||
* <p>一次性 {@code listByIds},不要逐单查:列表接口无分页,逐单查会变成 N+1。</p>
|
||||
*/
|
||||
private void attachTenderFiles(List<HjcOrder> orders) {
|
||||
if (orders == null || orders.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<HjcOrder> paidOrders = new ArrayList<>();
|
||||
List<Integer> projectIds = new ArrayList<>();
|
||||
for (HjcOrder order : orders) {
|
||||
if (!isPaid(order) || order.getProjectId() == null) {
|
||||
continue;
|
||||
}
|
||||
paidOrders.add(order);
|
||||
if (!projectIds.contains(order.getProjectId())) {
|
||||
projectIds.add(order.getProjectId());
|
||||
}
|
||||
}
|
||||
if (paidOrders.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<Integer, String> tenderFileOfProject = new HashMap<>(projectIds.size());
|
||||
for (HjcBidProject project : hjcBidProjectService.listByIds(projectIds)) {
|
||||
if (project != null) {
|
||||
tenderFileOfProject.put(project.getId(), project.getTenderFile());
|
||||
}
|
||||
}
|
||||
for (HjcOrder order : paidOrders) {
|
||||
order.setTenderFiles(HjcTenderFileUtil.parse(tenderFileOfProject.get(order.getProjectId())));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 给**单张**订单附带标书附件(订单详情用;项目对象已在手上,不再查库)。
|
||||
*
|
||||
* <p>调用方若还要把 {@code project.tenderFile} 抹掉,必须**先调本方法**再抹。</p>
|
||||
*/
|
||||
private void attachTenderFiles(HjcOrder order) {
|
||||
if (order == null || !isPaid(order)) {
|
||||
return;
|
||||
}
|
||||
HjcBidProject project = order.getProject();
|
||||
if (project == null && order.getProjectId() != null) {
|
||||
project = hjcBidProjectService.getById(order.getProjectId());
|
||||
}
|
||||
order.setTenderFiles(HjcTenderFileUtil.parse(project == null ? null : project.getTenderFile()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 这张订单是否**已付款**(下载权限的唯一依据)。
|
||||
*
|
||||
* <p>刻意**不**复用「已完成」的展示口径({@code payStatus == 1 || orderStatus == 1}):
|
||||
* 展示口径带兜底分支,而「能不能下载」必须绑在付款事实上。退款({@code payStatus == 3})
|
||||
* 与待支付都不算。</p>
|
||||
*/
|
||||
private boolean isPaid(HjcOrder order) {
|
||||
return order != null && order.getPayStatus() != null && order.getPayStatus() == 1;
|
||||
}
|
||||
|
||||
@Operation(summary = "后台-订单分页")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@hjcGuard.isAdmin()")
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.gxwebsoft.hjc.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 标书附件(下载用)。
|
||||
*
|
||||
* <p>来源是 {@code hjc_bid_project.tender_file}(一站式推送的 {@code files} 字段,
|
||||
* 形态见 {@link com.gxwebsoft.hjc.util.HjcTenderFileUtil}),由**后端**解析成结构化列表后
|
||||
* 随订单响应下发——两种运行端 + PC 端一共三处渲染,解析放前端必然分叉。</p>
|
||||
*
|
||||
* <p><b>只在 {@code payStatus == 1}(已支付)的订单上出现</b>:它是付费内容,
|
||||
* 见 {@code HjcOrderController#attachTenderFiles}。</p>
|
||||
*/
|
||||
@Data
|
||||
@Schema(name = "HjcTenderFileVo对象", description = "标书附件(下载用)")
|
||||
public class HjcTenderFileVo {
|
||||
|
||||
@Schema(description = "展示名(原始数据没有名称时取 URL 末段)")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "可下载地址")
|
||||
private String url;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.gxwebsoft.hjc.dto.HjcTenderFileVo;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -12,6 +13,7 @@ import lombok.EqualsAndHashCode;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 汇吉采标书订单
|
||||
@@ -105,4 +107,19 @@ public class HjcOrder implements Serializable {
|
||||
@Schema(description = "标书项目")
|
||||
@TableField(exist = false)
|
||||
private HjcBidProject project;
|
||||
|
||||
/**
|
||||
* 标书附件(**非表字段**,仅在 {@code payStatus == 1} 的订单上由接口填充)。
|
||||
*
|
||||
* <p>它是**付费内容**:待支付 / 支付失败 / 已退款的订单一律为 {@code null},
|
||||
* 门槛的落点是 {@code HjcOrderController#attachTenderFiles}(只有一处)。
|
||||
* 附件来源是项目的 {@code tender_file},由 {@link com.gxwebsoft.hjc.util.HjcTenderFileUtil}
|
||||
* 解析成结构化列表后再下发。</p>
|
||||
*
|
||||
* <p>另外:订单详情响应里 {@code project.tenderFile} 会被**恒置 null**,
|
||||
* 免得订单路径上留一条能绕过付费判定的旁路。</p>
|
||||
*/
|
||||
@Schema(description = "标书附件(仅已支付订单返回,非表字段)")
|
||||
@TableField(exist = false)
|
||||
private List<HjcTenderFileVo> tenderFiles;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package com.gxwebsoft.hjc.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.gxwebsoft.hjc.dto.HjcTenderFileVo;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 标书附件({@code hjc_bid_project.tender_file})的解析(纯函数,不依赖 Spring)。
|
||||
*
|
||||
* <p><b>为什么要解析</b>:这个字段是从一站式推送里原样落库的字符串,契约是
|
||||
* 「数组JSON或逗号分隔」({@code HjcOneStopProjectPush#files}),也就是至少三种形态:
|
||||
* 字符串数组、对象数组(带文件名)、逗号分隔。前端三处(h5 两个运行端 + PC)都要渲染它,
|
||||
* 解析放前端必然分叉;放在这里还能用单测把「坏数据」这一支钉住。</p>
|
||||
*
|
||||
* <p><b>绝不抛异常</b>:一个坏附件不能让整张订单列表打不开(与 OCR「识别失败不阻断」同一取舍)。
|
||||
* 无法识别的条目就地丢弃,能认多少给多少。</p>
|
||||
*/
|
||||
public final class HjcTenderFileUtil {
|
||||
|
||||
private HjcTenderFileUtil() {
|
||||
}
|
||||
|
||||
/** 取不出文件名时的兜底展示名 */
|
||||
public static final String DEFAULT_NAME = "标书附件";
|
||||
|
||||
/** 非 JSON 形态的分隔符:英文/中文逗号、分号、换行 */
|
||||
private static final String DELIMITERS = "[,,;;\\r\\n]";
|
||||
|
||||
/**
|
||||
* 把库里的原始字符串解析成附件列表。
|
||||
*
|
||||
* <p>规则:</p>
|
||||
* <ul>
|
||||
* <li>空白 → 空列表</li>
|
||||
* <li>以 {@code [} / {@code {} 开头 → 按 JSON 解析(数组元素可为字符串或对象);
|
||||
* <b>JSON 坏了就返回空列表,不回退到按逗号切分</b>——形如 JSON 的坏数据切出来的
|
||||
* 碎片不是地址,宁可没有</li>
|
||||
* <li>其余 → 按 {@link #DELIMITERS} 切分</li>
|
||||
* <li>地址为空白的条目丢弃;同名地址按 {@code url} 去重(保持首次出现的顺序)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param raw 库里的 {@code tender_file},可为 null
|
||||
* @return 非 null 的列表(可能为空)
|
||||
*/
|
||||
public static List<HjcTenderFileVo> parse(String raw) {
|
||||
List<HjcTenderFileVo> files = new ArrayList<>();
|
||||
if (StrUtil.isBlank(raw)) {
|
||||
return files;
|
||||
}
|
||||
String text = raw.trim();
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
if (text.startsWith("[")) {
|
||||
JSONArray array;
|
||||
try {
|
||||
array = JSON.parseArray(text);
|
||||
} catch (Exception e) {
|
||||
return files;
|
||||
}
|
||||
if (array == null) {
|
||||
return files;
|
||||
}
|
||||
for (int i = 0; i < array.size(); i++) {
|
||||
addFromJsonElement(array.get(i), seen, files);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
if (text.startsWith("{")) {
|
||||
JSONObject object;
|
||||
try {
|
||||
object = JSON.parseObject(text);
|
||||
} catch (Exception e) {
|
||||
return files;
|
||||
}
|
||||
if (object != null) {
|
||||
addFromJsonObject(object, seen, files);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
for (String piece : text.split(DELIMITERS)) {
|
||||
addFile(piece, null, seen, files);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
/** 数组的一个元素:字符串直接当地址;对象取 url/name;其余类型(数字/布尔/嵌套)丢弃 */
|
||||
private static void addFromJsonElement(Object element, Set<String> seen, List<HjcTenderFileVo> files) {
|
||||
if (element == null) {
|
||||
return;
|
||||
}
|
||||
if (element instanceof JSONObject) {
|
||||
addFromJsonObject((JSONObject) element, seen, files);
|
||||
return;
|
||||
}
|
||||
if (element instanceof CharSequence) {
|
||||
addFile(element.toString(), null, seen, files);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addFromJsonObject(JSONObject object, Set<String> seen, List<HjcTenderFileVo> files) {
|
||||
String url = firstNotBlank(object, "url", "fileUrl", "fileURL", "path", "downloadUrl");
|
||||
String name = firstNotBlank(object, "name", "fileName", "filename", "title");
|
||||
addFile(url, name, seen, files);
|
||||
}
|
||||
|
||||
private static void addFile(String url, String name, Set<String> seen, List<HjcTenderFileVo> files) {
|
||||
if (StrUtil.isBlank(url)) {
|
||||
return;
|
||||
}
|
||||
String trimmedUrl = url.trim();
|
||||
if (!seen.add(trimmedUrl)) {
|
||||
return;
|
||||
}
|
||||
HjcTenderFileVo vo = new HjcTenderFileVo();
|
||||
vo.setUrl(trimmedUrl);
|
||||
vo.setName(StrUtil.isBlank(name) ? nameFromUrl(trimmedUrl) : name.trim());
|
||||
files.add(vo);
|
||||
}
|
||||
|
||||
private static String firstNotBlank(JSONObject object, String... keys) {
|
||||
for (String key : keys) {
|
||||
String value = object.getString(key);
|
||||
if (StrUtil.isNotBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从地址里取一个能看的展示名:去掉 query/fragment 后取最后一段;取不到就用默认名。
|
||||
*
|
||||
* <p>末段若带百分号编码(中文文件名常见)就试着解码——解不开(不是合法编码)时原样返回,
|
||||
* 不因为一个名字让整条记录丢掉。</p>
|
||||
*/
|
||||
private static String nameFromUrl(String url) {
|
||||
String path = url;
|
||||
int cut = indexOfAny(path, '?', '#');
|
||||
if (cut >= 0) {
|
||||
path = path.substring(0, cut);
|
||||
}
|
||||
int slash = path.lastIndexOf('/');
|
||||
String name = slash >= 0 ? path.substring(slash + 1) : path;
|
||||
name = name.trim();
|
||||
if (name.isEmpty()) {
|
||||
return DEFAULT_NAME;
|
||||
}
|
||||
if (name.indexOf('%') >= 0) {
|
||||
try {
|
||||
name = URLDecoder.decode(name, "UTF-8").trim();
|
||||
} catch (Exception ignored) {
|
||||
// 不是合法的百分号编码:保留原样,好过丢一条附件
|
||||
}
|
||||
}
|
||||
return name.isEmpty() ? DEFAULT_NAME : name;
|
||||
}
|
||||
|
||||
private static int indexOfAny(String text, char a, char b) {
|
||||
int i = text.indexOf(a);
|
||||
int j = text.indexOf(b);
|
||||
if (i < 0) {
|
||||
return j;
|
||||
}
|
||||
if (j < 0) {
|
||||
return i;
|
||||
}
|
||||
return Math.min(i, j);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.gxwebsoft.hjc.controller;
|
||||
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.hjc.auth.HjcAdminGuard;
|
||||
import com.gxwebsoft.hjc.entity.HjcBidProject;
|
||||
import com.gxwebsoft.hjc.entity.HjcEnterprise;
|
||||
import com.gxwebsoft.hjc.service.HjcBidProjectService;
|
||||
@@ -18,7 +19,9 @@ import java.lang.reflect.Field;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -30,18 +33,23 @@ import static org.mockito.Mockito.when;
|
||||
* <p><b>最要紧的一条:未登录时 {@code favorited} 必须是 {@code false},而不是 401。</b>
|
||||
* 详情页对未登录用户是正常可用的页面(页面上本就有未登录分支),
|
||||
* 在这里返回 401 会把「收藏态查不到」升级成「整个详情页看不了」。</p>
|
||||
*
|
||||
* <p>本轮追加的护栏:{@code tenderFile}(标书附件地址)是付费内容——只有 hjc 管理员
|
||||
* (后台编辑表单要回填)或已为本项目付过款的买家才拿得到,其余一律 {@code null}。</p>
|
||||
*/
|
||||
class HjcBidProjectControllerDetailTest {
|
||||
|
||||
private static final int PROJECT_ID = 123;
|
||||
private static final int BUYER_USER_ID = 9001;
|
||||
private static final int BUYER_ENTERPRISE_ID = 7001;
|
||||
private static final String TENDER_FILE = "[\"https://oss.example.com/a/招标文件.pdf\"]";
|
||||
|
||||
private HjcBidProjectController controller;
|
||||
private HjcBidProjectService hjcBidProjectService;
|
||||
private HjcOrderService hjcOrderService;
|
||||
private HjcEnterpriseService hjcEnterpriseService;
|
||||
private HjcProjectFavoriteService hjcProjectFavoriteService;
|
||||
private HjcAdminGuard hjcGuard;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
@@ -50,15 +58,18 @@ class HjcBidProjectControllerDetailTest {
|
||||
hjcOrderService = mock(HjcOrderService.class);
|
||||
hjcEnterpriseService = mock(HjcEnterpriseService.class);
|
||||
hjcProjectFavoriteService = mock(HjcProjectFavoriteService.class);
|
||||
hjcGuard = mock(HjcAdminGuard.class);
|
||||
|
||||
inject("hjcBidProjectService", hjcBidProjectService);
|
||||
inject("hjcOrderService", hjcOrderService);
|
||||
inject("hjcEnterpriseService", hjcEnterpriseService);
|
||||
inject("hjcProjectFavoriteService", hjcProjectFavoriteService);
|
||||
inject("hjcGuard", hjcGuard);
|
||||
|
||||
HjcBidProject project = new HjcBidProject();
|
||||
project.setId(PROJECT_ID);
|
||||
project.setProjectName("某测试项目");
|
||||
project.setTenderFile(TENDER_FILE);
|
||||
when(hjcBidProjectService.getById(PROJECT_ID)).thenReturn(project);
|
||||
when(hjcOrderService.countPaidOrders(PROJECT_ID)).thenReturn(3);
|
||||
}
|
||||
@@ -148,4 +159,64 @@ class HjcBidProjectControllerDetailTest {
|
||||
|
||||
assertEquals(1, controller.detail(999).getCode());
|
||||
}
|
||||
|
||||
// ---------- 标书附件地址:付费内容,未购买拿不到 ----------
|
||||
|
||||
@Test
|
||||
void anonymousShouldNotGetTenderFile() {
|
||||
ApiResult<?> res = controller.detail(PROJECT_ID);
|
||||
|
||||
assertEquals(0, res.getCode(), "抹掉附件地址不等于把详情打回 401");
|
||||
assertNull(projectOf(res).getTenderFile(), "匿名不能拿到标书附件地址");
|
||||
verify(hjcOrderService, never()).listPaidProjectIds(anyInt(), anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loggedInWithoutEnterpriseShouldNotGetTenderFile() {
|
||||
loginAsBuyer();
|
||||
when(hjcEnterpriseService.getByUserId(BUYER_USER_ID)).thenReturn(null);
|
||||
|
||||
ApiResult<?> res = controller.detail(PROJECT_ID);
|
||||
|
||||
assertNull(projectOf(res).getTenderFile());
|
||||
verify(hjcOrderService, never()).listPaidProjectIds(anyInt(), anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loggedInNotPurchasedShouldNotGetTenderFile() {
|
||||
loginAsBuyer();
|
||||
HjcEnterprise e = new HjcEnterprise();
|
||||
e.setId(BUYER_ENTERPRISE_ID);
|
||||
when(hjcEnterpriseService.getByUserId(BUYER_USER_ID)).thenReturn(e);
|
||||
// 默认 mock 返回空集合 = 没买过
|
||||
|
||||
ApiResult<?> res = controller.detail(PROJECT_ID);
|
||||
|
||||
assertNull(projectOf(res).getTenderFile(), "没付款就不能拿到标书附件地址");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loggedInPurchasedShouldGetTenderFile() {
|
||||
loginAsBuyer();
|
||||
HjcEnterprise e = new HjcEnterprise();
|
||||
e.setId(BUYER_ENTERPRISE_ID);
|
||||
when(hjcEnterpriseService.getByUserId(BUYER_USER_ID)).thenReturn(e);
|
||||
when(hjcOrderService.listPaidProjectIds(BUYER_ENTERPRISE_ID, Collections.singletonList(PROJECT_ID)))
|
||||
.thenReturn(Collections.singletonList(PROJECT_ID));
|
||||
|
||||
ApiResult<?> res = controller.detail(PROJECT_ID);
|
||||
|
||||
assertEquals(TENDER_FILE, projectOf(res).getTenderFile(), "已付款买家应当能拿到附件地址");
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminShouldGetTenderFileWithoutPurchaseCheck() {
|
||||
loginAsBuyer();
|
||||
when(hjcGuard.isAdmin()).thenReturn(true);
|
||||
|
||||
ApiResult<?> res = controller.detail(PROJECT_ID);
|
||||
|
||||
assertEquals(TENDER_FILE, projectOf(res).getTenderFile(), "后台编辑表单要靠它回填");
|
||||
verify(hjcOrderService, never()).listPaidProjectIds(anyInt(), anyList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
package com.gxwebsoft.hjc.controller;
|
||||
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.gxwebsoft.hjc.auth.HjcAdminGuard;
|
||||
import com.gxwebsoft.hjc.entity.HjcBidProject;
|
||||
import com.gxwebsoft.hjc.entity.HjcEnterprise;
|
||||
import com.gxwebsoft.hjc.entity.HjcOrder;
|
||||
import com.gxwebsoft.hjc.service.HjcBidProjectService;
|
||||
import com.gxwebsoft.hjc.service.HjcBizService;
|
||||
import com.gxwebsoft.hjc.service.HjcEnterpriseService;
|
||||
import com.gxwebsoft.hjc.service.HjcOrderService;
|
||||
import com.gxwebsoft.payment.service.PaymentService;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 「已支付订单才有标书附件」这条门槛的护栏({@code my} 与 {@code detail} 两个出口)。
|
||||
*
|
||||
* <p><b>为什么必须用 mock 测</b>:这条门槛最要紧的几支在 dev 里造不出来——要一张真实已付款订单、
|
||||
* 要一张已退款订单,还要有附件数据的项目。而它一旦失效(未支付也拿到地址),
|
||||
* 就是把付费内容白送出去,属于「宁可多测一遍」的那类改动。</p>
|
||||
*/
|
||||
class HjcOrderControllerTenderFilesTest {
|
||||
|
||||
private static final int ORDER_ID = 88;
|
||||
private static final int PROJECT_ID = 66;
|
||||
private static final int BUYER_USER_ID = 9001;
|
||||
private static final int BUYER_ENTERPRISE_ID = 7001;
|
||||
private static final String FILES = "[\"https://oss.example.com/a/招标文件.pdf\"]";
|
||||
|
||||
private HjcOrderController controller;
|
||||
private HjcOrderService hjcOrderService;
|
||||
private HjcBidProjectService hjcBidProjectService;
|
||||
private HjcEnterpriseService hjcEnterpriseService;
|
||||
private HjcAdminGuard hjcGuard;
|
||||
|
||||
/**
|
||||
* {@code my()} 用 {@code LambdaQueryWrapper} 取列表,而 lambda 的列名解析依赖 MyBatis-Plus 的
|
||||
* {@code TableInfo} 缓存;单元测试里没有 Spring/MyBatis 环境(不启动容器就没有 mapper 扫描),
|
||||
* 不手动注册实体的话,构造 wrapper 时就会抛
|
||||
* {@code can not find lambda cache for this entity}。
|
||||
*/
|
||||
@BeforeAll
|
||||
static void initMybatisPlusTableInfo() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), HjcOrder.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
controller = new HjcOrderController();
|
||||
hjcOrderService = mock(HjcOrderService.class);
|
||||
hjcBidProjectService = mock(HjcBidProjectService.class);
|
||||
hjcEnterpriseService = mock(HjcEnterpriseService.class);
|
||||
hjcGuard = mock(HjcAdminGuard.class);
|
||||
|
||||
inject("hjcOrderService", hjcOrderService);
|
||||
inject("hjcBidProjectService", hjcBidProjectService);
|
||||
inject("hjcEnterpriseService", hjcEnterpriseService);
|
||||
inject("hjcGuard", hjcGuard);
|
||||
inject("hjcBizService", mock(HjcBizService.class));
|
||||
inject("paymentService", mock(PaymentService.class));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
private void inject(String field, Object value) throws Exception {
|
||||
Field f = HjcOrderController.class.getDeclaredField(field);
|
||||
f.setAccessible(true);
|
||||
f.set(controller, value);
|
||||
}
|
||||
|
||||
private void loginAsBuyer() {
|
||||
User u = new User();
|
||||
u.setUserId(BUYER_USER_ID);
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(u, null, Collections.emptyList()));
|
||||
when(hjcGuard.currentUser()).thenReturn(u);
|
||||
when(hjcGuard.isAdmin()).thenReturn(false);
|
||||
HjcEnterprise enterprise = new HjcEnterprise();
|
||||
enterprise.setId(BUYER_ENTERPRISE_ID);
|
||||
when(hjcEnterpriseService.getByUserId(BUYER_USER_ID)).thenReturn(enterprise);
|
||||
}
|
||||
|
||||
private HjcOrder order(Integer payStatus) {
|
||||
HjcOrder o = new HjcOrder();
|
||||
o.setId(ORDER_ID);
|
||||
o.setOrderNo("HJC202609170000000001");
|
||||
o.setProjectId(PROJECT_ID);
|
||||
o.setEnterpriseId(BUYER_ENTERPRISE_ID);
|
||||
o.setPayStatus(payStatus);
|
||||
o.setOrderStatus(payStatus != null && payStatus == 1 ? 1 : 0);
|
||||
return o;
|
||||
}
|
||||
|
||||
private HjcBidProject project() {
|
||||
HjcBidProject p = new HjcBidProject();
|
||||
p.setId(PROJECT_ID);
|
||||
p.setProjectName("某测试项目");
|
||||
p.setTenderFile(FILES);
|
||||
return p;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<HjcOrder> ordersOf(ApiResult<?> res) {
|
||||
return (List<HjcOrder>) res.getData();
|
||||
}
|
||||
|
||||
// ---------- 我的订单 ----------
|
||||
|
||||
@Test
|
||||
void myShouldAttachFilesOnlyForPaidOrders() {
|
||||
loginAsBuyer();
|
||||
HjcOrder paid = order(1);
|
||||
HjcOrder pending = order(0);
|
||||
HjcOrder refunded = order(3);
|
||||
when(hjcOrderService.list(any())).thenReturn(Arrays.asList(paid, pending, refunded));
|
||||
when(hjcBidProjectService.listByIds(any())).thenReturn(Collections.singletonList(project()));
|
||||
|
||||
ApiResult<?> res = controller.my();
|
||||
|
||||
assertEquals(0, res.getCode());
|
||||
List<HjcOrder> list = ordersOf(res);
|
||||
assertEquals(1, list.get(0).getTenderFiles().size(), "已支付必须拿到附件");
|
||||
assertEquals("招标文件.pdf", list.get(0).getTenderFiles().get(0).getName());
|
||||
assertNull(list.get(1).getTenderFiles(), "待支付不给附件(null 与空列表是两件事)");
|
||||
assertNull(list.get(2).getTenderFiles(), "已退款不给附件");
|
||||
}
|
||||
|
||||
@Test
|
||||
void myShouldQueryProjectsOnceForAllPaidOrders() {
|
||||
loginAsBuyer();
|
||||
when(hjcOrderService.list(any())).thenReturn(Arrays.asList(order(1), order(1), order(0)));
|
||||
when(hjcBidProjectService.listByIds(any())).thenReturn(Collections.singletonList(project()));
|
||||
|
||||
controller.my();
|
||||
|
||||
List<Integer> expectedIds = new ArrayList<>();
|
||||
expectedIds.add(PROJECT_ID);
|
||||
verify(hjcBidProjectService, times(1)).listByIds(expectedIds);
|
||||
}
|
||||
|
||||
@Test
|
||||
void myWithoutPaidOrdersShouldNotQueryProjects() {
|
||||
loginAsBuyer();
|
||||
when(hjcOrderService.list(any())).thenReturn(Collections.singletonList(order(0)));
|
||||
|
||||
controller.my();
|
||||
|
||||
verify(hjcBidProjectService, times(0)).listByIds(any());
|
||||
}
|
||||
|
||||
// ---------- 订单详情 ----------
|
||||
|
||||
@Test
|
||||
void detailOfPaidOrderShouldAttachFilesAndHideProjectTenderFile() {
|
||||
loginAsBuyer();
|
||||
HjcOrder paid = order(1);
|
||||
when(hjcOrderService.getById(ORDER_ID)).thenReturn(paid);
|
||||
when(hjcBidProjectService.getById(PROJECT_ID)).thenReturn(project());
|
||||
|
||||
ApiResult<?> res = controller.detail(ORDER_ID);
|
||||
|
||||
assertEquals(0, res.getCode());
|
||||
HjcOrder detail = (HjcOrder) res.getData();
|
||||
assertNotNull(detail.getTenderFiles());
|
||||
assertEquals(1, detail.getTenderFiles().size());
|
||||
assertNull(detail.getProject().getTenderFile(),
|
||||
"订单路径不留第二条地址来源:一律抹掉 project.tenderFile");
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailOfPendingOrderShouldGiveNoFiles() {
|
||||
loginAsBuyer();
|
||||
HjcOrder pending = order(0);
|
||||
when(hjcOrderService.getById(ORDER_ID)).thenReturn(pending);
|
||||
when(hjcBidProjectService.getById(PROJECT_ID)).thenReturn(project());
|
||||
|
||||
ApiResult<?> res = controller.detail(ORDER_ID);
|
||||
|
||||
assertEquals(0, res.getCode());
|
||||
HjcOrder detail = (HjcOrder) res.getData();
|
||||
assertNull(detail.getTenderFiles(), "未支付不能拿到附件地址");
|
||||
assertNull(detail.getProject().getTenderFile(), "未支付也不能从 project 里绕到地址");
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailOfRefundedOrderShouldGiveNoFiles() {
|
||||
loginAsBuyer();
|
||||
HjcOrder refunded = order(3);
|
||||
when(hjcOrderService.getById(ORDER_ID)).thenReturn(refunded);
|
||||
when(hjcBidProjectService.getById(PROJECT_ID)).thenReturn(project());
|
||||
|
||||
ApiResult<?> res = controller.detail(ORDER_ID);
|
||||
|
||||
HjcOrder detail = (HjcOrder) res.getData();
|
||||
assertNull(detail.getTenderFiles(), "已退款视为不再提供下载");
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailWithoutProjectShouldStillSucceed() {
|
||||
loginAsBuyer();
|
||||
when(hjcOrderService.getById(ORDER_ID)).thenReturn(order(1));
|
||||
when(hjcBidProjectService.getById(PROJECT_ID)).thenReturn(null);
|
||||
|
||||
ApiResult<?> res = controller.detail(ORDER_ID);
|
||||
|
||||
assertEquals(0, res.getCode(), "项目被删不该让订单详情打不开");
|
||||
assertTrue(((HjcOrder) res.getData()).getTenderFiles().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void otherEnterpriseOrderShouldStillBeForbidden() {
|
||||
loginAsBuyer();
|
||||
HjcOrder other = order(1);
|
||||
other.setEnterpriseId(9999);
|
||||
when(hjcOrderService.getById(ORDER_ID)).thenReturn(other);
|
||||
|
||||
assertEquals(403, controller.detail(ORDER_ID).getCode());
|
||||
verify(hjcBidProjectService, times(0)).getById(anyInt());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.gxwebsoft.hjc.util;
|
||||
|
||||
import com.gxwebsoft.hjc.dto.HjcTenderFileVo;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 标书附件解析的护栏。
|
||||
*
|
||||
* <p>这里的每一种形态都来自真实数据契约(一站式推送的 {@code files}「数组JSON或逗号分隔」),
|
||||
* 而**坏数据这一支在端到端环境里造不出来**(要往库里塞脏字符串),只能靠单测钉住。</p>
|
||||
*/
|
||||
class HjcTenderFileUtilTest {
|
||||
|
||||
@Test
|
||||
void blankShouldGiveEmptyList() {
|
||||
assertTrue(HjcTenderFileUtil.parse(null).isEmpty());
|
||||
assertTrue(HjcTenderFileUtil.parse("").isEmpty());
|
||||
assertTrue(HjcTenderFileUtil.parse(" ").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringArrayShouldBeParsedWithUrlTailAsName() {
|
||||
List<HjcTenderFileVo> files = HjcTenderFileUtil.parse(
|
||||
"[\"https://oss.example.com/a/招标文件.pdf\",\"https://oss.example.com/b/清单.docx\"]");
|
||||
|
||||
assertEquals(2, files.size());
|
||||
assertEquals("招标文件.pdf", files.get(0).getName());
|
||||
assertEquals("https://oss.example.com/a/招标文件.pdf", files.get(0).getUrl());
|
||||
assertEquals("清单.docx", files.get(1).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void objectArrayShouldPreferExplicitName() {
|
||||
List<HjcTenderFileVo> files = HjcTenderFileUtil.parse(
|
||||
"[{\"url\":\"https://oss.example.com/1.pdf\",\"name\":\"招标文件(正式版).pdf\"}]");
|
||||
|
||||
assertEquals(1, files.size());
|
||||
assertEquals("招标文件(正式版).pdf", files.get(0).getName());
|
||||
assertEquals("https://oss.example.com/1.pdf", files.get(0).getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void objectArrayShouldAcceptAlternativeKeys() {
|
||||
List<HjcTenderFileVo> files = HjcTenderFileUtil.parse(
|
||||
"[{\"fileUrl\":\"https://oss.example.com/1.pdf\",\"fileName\":\"附件A.pdf\"},"
|
||||
+ "{\"path\":\"https://oss.example.com/2.pdf\",\"filename\":\"附件B.pdf\"}]");
|
||||
|
||||
assertEquals(2, files.size());
|
||||
assertEquals("附件A.pdf", files.get(0).getName());
|
||||
assertEquals("附件B.pdf", files.get(1).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleJsonObjectShouldBeTreatedAsOneFile() {
|
||||
List<HjcTenderFileVo> files = HjcTenderFileUtil.parse(
|
||||
"{\"url\":\"https://oss.example.com/only.pdf\"}");
|
||||
|
||||
assertEquals(1, files.size());
|
||||
assertEquals("only.pdf", files.get(0).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void commaAndNewlineSeparatedShouldBeParsed() {
|
||||
assertEquals(2, HjcTenderFileUtil.parse(
|
||||
"https://oss.example.com/a.pdf,https://oss.example.com/b.pdf").size());
|
||||
assertEquals(2, HjcTenderFileUtil.parse(
|
||||
"https://oss.example.com/a.pdf,https://oss.example.com/b.pdf").size(), "中文逗号也要认");
|
||||
assertEquals(2, HjcTenderFileUtil.parse(
|
||||
"https://oss.example.com/a.pdf\nhttps://oss.example.com/b.pdf").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void brokenJsonShouldGiveEmptyListInsteadOfThrowing() {
|
||||
assertTrue(HjcTenderFileUtil.parse("[坏 JSON").isEmpty(), "坏数据不能抛异常,也不能切成碎片当地址");
|
||||
assertTrue(HjcTenderFileUtil.parse("{oops").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unusableElementsShouldBeDropped() {
|
||||
List<HjcTenderFileVo> files = HjcTenderFileUtil.parse(
|
||||
"[\"https://oss.example.com/a.pdf\",null,\"\",123,true,{\"name\":\"没有地址\"}]");
|
||||
|
||||
assertEquals(1, files.size(), "只有带地址的字符串算一条附件");
|
||||
assertEquals("https://oss.example.com/a.pdf", files.get(0).getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicatedUrlShouldAppearOnce() {
|
||||
List<HjcTenderFileVo> files = HjcTenderFileUtil.parse(
|
||||
"[\"https://oss.example.com/a.pdf\",\"https://oss.example.com/a.pdf\"]");
|
||||
|
||||
assertEquals(1, files.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryStringShouldNotPolluteTheName() {
|
||||
List<HjcTenderFileVo> files = HjcTenderFileUtil.parse(
|
||||
"https://oss.example.com/a.pdf?sign=abc&expires=1");
|
||||
|
||||
assertEquals("a.pdf", files.get(0).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void encodedNameShouldBeDecoded() {
|
||||
List<HjcTenderFileVo> files = HjcTenderFileUtil.parse(
|
||||
"https://oss.example.com/%E6%8B%9B%E6%A0%87%E6%96%87%E4%BB%B6.pdf");
|
||||
|
||||
assertEquals("招标文件.pdf", files.get(0).getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void urlWithoutTailShouldFallBackToDefaultName() {
|
||||
List<HjcTenderFileVo> files = HjcTenderFileUtil.parse("https://oss.example.com/");
|
||||
|
||||
assertEquals(1, files.size());
|
||||
assertEquals(HjcTenderFileUtil.DEFAULT_NAME, files.get(0).getName());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user