完整教程:【JUnit实战3_22】 第十三章:用 JUnit 5 做持续集成(下):Jenkins + JUnit 5 + Git 持续集成本地实战演练完整复盘

news/2025/11/29 11:20:46/文章来源:https://www.cnblogs.com/gccbuaa/p/19285608

JUnit in Action, Third Edition

《JUnit in Action》全新第3版封面截图

写在前面
上篇讲了,第十三章的后半部分内容围绕 Jenkins 如何开展持续集成进行了演示,但中途由于牵涉太多 Git 工作流和相关配置细节的问题,很多重要节点没能详细展开。本篇根据书中思路在本地进行了完整实战演练,并对当中的详细配置细节及遇到的问题进行了深入分析,以便今后复盘。

第十三章:用 JUnit 5 做持续集成

(接上篇)

13.4 CI 实战:在 Jenkins 创建项目并处理构建任务

本章后续内容就是模拟了一个用 Jenkins 做持续集成的小案例,具体场景即示例公司 Tested Data Systems 的航班管理应用,涉及两个实体类的交互:FlightPassenger,分别由两位开发者独立维护。航班对象有一个名为 passengers 的乘客集合,并且提供了实例方法来动态添加或删除乘客元素;此外还有一些校验逻辑,限制乘客总数不超过总的座位数。

模拟的工作流是这样的:两位开发者先实现各自的实体类和测试类,运行无误后完成了代码版本的初始提交。后来维护 Passenger 的开发者决定新增一个实例方法,让乘客也能主动选择某个航班,而不是只能被动地接受航班的指派。结果修改完代码并提交到代码库后,Jenkins 构建失败了,原因是 Flight 及其测试类也需要同步修改一些细节。于是,维护 Flight 的开发者又重新签出项目代码,修复 Bug 并提交了一个新的版本到 Jenkins。最后通过了 Jenkins 的手动构建任务。

这个流程涉及 Jenkins 的相关配置、Git 本地代码库的临时搭建、多次版本提交的演示、以及 Jenkins 构建任务的执行。可能是篇幅有限,作者并没有详细展开演示每一个步骤,很多地方都是一带而过。作为以实战为主的自学笔记,肯定是需要亲自动手尝试的,一来看看书中有没有遗漏的关键节点,二来也可以验证一下五年后的新版本新环境下会遇到哪些具体的新问题。

以下是具体实测过程。

13.4.1 构建示例项目

在桌面新建文件夹 ch13_ci_demo 作为项目的本地 Git 仓库。先完成基础配置:

# 在桌面创建一个空的示例文件夹,用于存放本地 Git 代码库,以及模拟两个开发者的本地工作空间(稍后实现)
> (pwd).Path
F:\mydesktop
> mkdir ch13_ci_demo | Out-Null
> cd ch13_ci_demo
# 用 Maven 命令快速初始化示例项目
> mvn archetype:generate -DgroupId="com.manning.junitbook" -DartifactId="ch13-ci-demo" -DarchetypeArtifactid="maven-artifact-mojo"

关于 Maven 命令中的两个细节

注意:在 PowerShell 命令行中,各参数的值最好用双引号标注一下(如 "com.manning.junitbook"),以免运行报错。如果是 CMD 命令行环境则可以不用加注。

另外,上述 Maven 命令在最后确认环节如果需要调整某些配置,只需输入一个 n,再按回车确认,即可逐一审核每个初始参数。例如初始配置的 Java 版本是 17,我本地只有 21,于是就有手动修改环节:

Fig13.6

然后用 IDEA 打开该项目:

Fig13.7

然后根据书中步骤,完成 Flight 实体、Passenger 实体及其各自的测试类的创建:

package com.manning.junitbook.flights;
import com.manning.junitbook.passengers.Passenger;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Flight {
private final String flightNumber;
private final int seats;
private final Set<Passenger> passengers = new HashSet<>();private static final String flightNumberRegex = "^[A-Z]{2}\\d{3,4}$";private static final Pattern pattern = Pattern.compile(flightNumberRegex);public Flight(String flightNumber, int seats) {Matcher matcher = pattern.matcher(flightNumber);if (!matcher.matches()) {throw new RuntimeException("Invalid flight number");}this.flightNumber = flightNumber;this.seats = seats;}public String getFlightNumber() {return flightNumber;}public int getNumberOfPassengers() {return passengers.size();}public boolean addPassenger(Passenger passenger) {if (getNumberOfPassengers() >= seats) {throw new RuntimeException("Not enough seats for flight " + getFlightNumber());}return passengers.add(passenger);}public boolean removePassenger(Passenger passenger) {return passengers.remove(passenger);}}

初始测试类 FlightTest

package com.manning.junitbook.flights;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class FlightTest {
@Test
public void testFlightCreation() {
Flight flight = new Flight("AA123", 100);
assertNotNull(flight);
}
@Test
public void testInvalidFlightNumber() {
assertThrows(RuntimeException.class,
() -> new Flight("AA12", 100));
}
}

乘客实体类 Passenger

package com.manning.junitbook.passengers;
import java.util.Arrays;
import java.util.Locale;
public class Passenger {
private final String identifier;
private final String name;
private final String countryCode;
public Passenger(String identifier, String name, String countryCode) {
if (!Arrays.asList(Locale.getISOCountries()).contains(countryCode)) {
throw new RuntimeException("Invalid country code");
}
this.identifier = identifier;
this.name = name;
this.countryCode = countryCode;
}
public String getIdentifier() {
return identifier;
}
public String getName() {
return name;
}
public String getCountryCode() {
return countryCode;
}
@Override
public String toString() {
return "Passenger " + getName() + " with identifier: " + getIdentifier() + " from " + getCountryCode();
}
}

对应的初始测试类 PassengerTest

package com.manning.junitbook.passengers;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class PassengerTest {
@Test
void testPassengerCreation() {
Passenger passenger = new Passenger("123-45-6789", "John Smith", "US");
assertNotNull(passenger);
}
@Test
void testInvalidCountryCode() {
assertThrows(RuntimeException.class,
() -> new Passenger("900-45-6789", "John Smith", "GJ"));
}
@Test
void testPassengerToString() {
Passenger passenger = new Passenger("123-45-6789", "John Smith", "US");
assertEquals("Passenger John Smith with identifier: 123-45-6789 from US", passenger.toString());
}
}

由于 Flight 航班实体可以控制预订该航班乘客的数量,因此负责实现 Flight 的开发者又新增了一个集成测试类 FlightWithPassengersTest

package com.manning.junitbook.flightspassengers;
import com.manning.junitbook.flights.Flight;
import com.manning.junitbook.passengers.Passenger;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class FlightWithPassengersTest {
private final Flight flight = new Flight("AA123", 1);
@Test
void testAddRemovePassengers() {
Passenger passenger = new Passenger("124-56-7890", "Michael Johnson", "US");
assertTrue(flight.addPassenger(passenger));
assertEquals(1, flight.getNumberOfPassengers ());
assertTrue(flight.removePassenger(passenger));
assertEquals(0, flight.getNumberOfPassengers ());
}
@Test
void testNumberOfSeats() {
Passenger passenger1 = new Passenger("124-56-7890", "Michael Johnson", "US");
flight.addPassenger(passenger1);
assertEquals(1, flight.getNumberOfPassengers ());
Passenger passenger2 = new Passenger("127-23-7991", "John Smith", "GB");
final RuntimeException ex = assertThrows(RuntimeException.class, () -> flight.addPassenger(passenger2));
assertEquals("Not enough seats for flight AA123", ex.getMessage());
}
}

最后运行测试,为下一步 Git 仓库的初始化做准备:

Fig13.8

13.4.2 Git 仓库初始化

根据书中的演示内容,远程仓库的地址是 \\192.168.1.5\ch13-continuous。这就需要在本地创建一个 Git 远程仓库,并共享该仓库,好让后续两个开发者签出代码。具体操作如下:

# 导航到示例项目根目录下
> cd F:/mydesktop/ch13_ci_demo/ch13-ci-demo
# 初始化 Git 仓库
> git init
# 添加 .gitignore 忽略文件,筛除无关内容
> vim .gitignore
> cat .gitignore
.idea
.mvn
target
# 添加本地变更
> git add *
# 生成首个本地提交版本
> git commit -m 'init commit'
[master (root-commit) 7c23e4d] init repo
7 files changed, 255 insertions(+)
create mode 100644 .gitignore
create mode 100644 pom.xml
create mode 100644 src/main/java/com/manning/junitbook/flights/Flight.java
create mode 100644 src/main/java/com/manning/junitbook/passengers/Passenger.java
create mode 100644 src/test/java/com/manning/junitbook/flights/FlightTest.java
create mode 100644 src/test/java/com/manning/junitbook/flightspassengers/FlightWithPassengersTest.java
create mode 100644 src/test/java/com/manning/junitbook/passengers/PassengerTest.java

接下来这一步很关键,要在本地模拟一个 Git 远程仓库,名称为 ch13-continuous

# 生成模拟远程仓库
> git init --bare ../ch13-continuous
Initialized empty Git repository in F:/mydesktop/ch13_ci_demo/ch13-continuous/
# 关联本地工作目录与远程仓库
> git remote add origin "F:\mydesktop\ch13_ci_demo\ch13-continuous"
# 推送本地变更到远程库
> git push -u origin master
Enumerating objects: 25, done.
Counting objects: 100% (25/25), done.
Delta compression using up to 16 threads
Compressing objects: 100% (12/12), done.
Writing objects: 100% (25/25), 3.74 KiB | 637.00 KiB/s, done.
Total 25 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To F:\mydesktop\ch13_ci_demo\ch13-continuous
* [new branch]      master -> master
branch 'master' set up to track 'origin/master'.

然后共享该远程仓库,得到远程仓库的访问地址(即 \\TX2\ch13-continuous):

Fig13.9

Fig13.10

然后就可以模拟两个开发者的工作目录了(devPassengerdevFlight):

> (pwd).Path
F:\mydesktop\ch13_ci_demo
# 模拟维护 Passenger 的开发者 Beth,其本地工作目录命名为 devPassenger
> git clone \\TX2\ch13-continuous devPassenger
Cloning into 'devPassenger'...
done.
> cd devPassenger
> git config user.name Beth
> git config user.email "beth@example.com"
# 同理,模拟维护 Flight 的开发者 John,其本地工作目录命名为 devFlight
> git clone \\TX2\ch13-continuous devFlight
Cloning into 'devFlight'...
done.
> cd devFlight
> git config user.name John
> git config user.email "john@example.com"

对上述操作不熟的朋友可以参考我的 Git笔记专栏。

13.4.3 用 Jenkins 完成首次构建

这里实测时遇到个大坑:Windows 自动安装的 Jenkins 服务始终无法正常构建项目,因为出于安全考虑中止了本地共享文件夹做 Git 远程仓库的相关操作:

Fig13.11

而按照截图中的提示,启动时需要添加一个系统属性 hudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT,值设为 true 就能运行。但无论是修改 Jenkins 安装目录下的配置文件 jenkins.xml,还是从 WebUI 界面运行 Groovy 脚本 System.setProperty('hudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT', 'true') 都不生效;唯一生效的方案是在 CMD 命令行用 java -jar 启动:

java -Dhudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT=true -jar jenkins.war

这种方式也要重新安装插件(如图所示)、重新初始化登录帐号密码等例行步骤:

Fig13.1

接着来到 新建Item 的配置页(http://localhost:8080/job/ch13-continuous/configure),在源码管理栏填写刚建好的远程 Git 仓库地址:

Fig13.13

然后在 Build Steps 栏输入构建命令 clean install

Fig13.14

保存配置,就得到一个名为 ch13-continuous 的构建目标:

Fig13.15

点击右边的运行按钮,完成首次构建:

Fig13.16

稍等片刻,刷新页面会看到状态已经更新(表示构建成功):

Fig13.17

13.4.4 新增乘客主动选择航班接口

IDEA 打开由开发者 A 维护的工作目录 devPassenger,在 Passenger 实体类中新增一个实例方法 public void joinFlight(Flight flight),让乘客可以自主选择某个航班:

public class Passenger {
// -- snip --
private Flight flight;
public Flight getFlight() {
return flight;
}
public void setFlight(Flight flight) {
this.flight = flight;
}
public void joinFlight(Flight flight) {
Flight previousFlight = this.flight;
if (null != previousFlight) {
if (!previousFlight.removePassenger(this)) {
throw new RuntimeException("Cannot remove passenger");
}
}
setFlight(flight);
if (null != flight) {
if (!flight.addPassenger(this)) {
throw new RuntimeException("Cannot add passenger");
}
}
}
// -- snip --
}

这里一定要十分仔细,因为 FlightPassenger 此时实现了双向通信,作为参数传入 joinFlight() 方法的 flight 对象也可以直接访问预订该航班的乘客集合 passengers,因此在第 L20 行指定新的航班前后,都需要 同步更新 前一航班及当前航班的乘客集合。

同时,集成测试类也要新增相应的测试用例,并微调之前的测试逻辑:

public class FlightWithPassengersTest {
// -- snip --
@Test
public void testPassengerJoinsFlight() {
Passenger passenger = new Passenger("123-45-6789", "John Smith", "US");
Flight flight = new Flight("AA123", 100);
passenger.joinFlight(flight);
assertEquals(flight, passenger.getFlight());
assertEquals(1, flight.getNumberOfPassengers());
}
@Test
void testAddRemovePassengers() {
Passenger passenger = new Passenger("124-56-7890", "Michael Johnson", "US");
assertTrue(flight.addPassenger(passenger));
assertEquals(1, flight.getNumberOfPassengers ());
assertEquals(flight, passenger.getFlight());
assertTrue(flight.removePassenger(passenger));
assertEquals(0, flight.getNumberOfPassengers ());
assertNull(passenger.getFlight());
}
// -- snip --
}

13.4.5 用 Jenkins 完成第二次构建

为了演示构建失败的效果,这里故意跳过了本地测试,直接推送到远程仓库:

> (pwd).Path
F:\mydesktop\ch13_ci_demo\devPassenger
> git add *.java
> git commit -m 'Allow the passenger to make the individual choice of a flight'
[master e5d238a] Allow the passenger to make the individual choice of a flight
2 files changed, 38 insertions(+)
# 推送到远程仓库
> git push
Enumerating objects: 31, done.
Counting objects: 100% (31/31), done.
Delta compression using up to 16 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (17/17), 1.43 KiB | 366.00 KiB/s, done.
Total 17 (delta 3), reused 0 (delta 0), pack-reused 0 (from 0)
To \\TX2\ch13-continuous
7c23e4d..e5d238a  master -> master
# 确认推送结果
> git log --oneline --graph --decorate --all
* e5d238a (HEAD -> master, origin/master, origin/HEAD) Allow the passenger to make the individual choice of a flight
* 7c23e4d init repo
>

回到 Jenkins 页面,重新构建项目失败了:

Fig13.18

从编号为 #2 的构建记录中查看报错原因,发现有个为空的断言失败了:

Fig13.19

IDEA 中再次定位报错断言:

Fig13.20

13.4.6 修复集成测试中的 Bug

经分析,是 Flight 在管理乘客时没有对该乘客所在的航班进行同步关联,这需要另一名开发者进行修改。于是负责 Flight 的开发者 John 先同步最新的代码到他的工作空间:

> (pwd).Path
F:\mydesktop\ch13_ci_demo\devFlight
> git pull
From \\TX2\ch13-continuous
+ 3a43104...e5d238a master     -> origin/master  (forced update)
Updating 7c23e4d..e5d238a
Fast-forward
.../manning/junitbook/passengers/Passenger.java    | 26 ++++++++++++++++++++++
.../FlightWithPassengersTest.java                  | 12 ++++++++++
2 files changed, 38 insertions(+)
>

再对 addPassenger()removePassenger() 修改如下(L7L12):

public class Flight {
// -- snip --
public boolean addPassenger(Passenger passenger) {
if (getNumberOfPassengers() >= seats) {
throw new RuntimeException("Not enough seats for flight " + getFlightNumber());
}
passenger.setFlight(this);
return passengers.add(passenger);
}
public boolean removePassenger(Passenger passenger) {
passenger.setFlight(null);
return passengers.remove(passenger);
}
}

在本地运行 mvn test 确认无误后,再推送到远程:

> git add *.java
> git commit -m 'Adding integration code for a passenger join/unjoin'
[master 3a43104] Adding integration code for a passenger join/unjoin
1 file changed, 2 insertions(+)
> git push
Enumerating objects: 19, done.
Counting objects: 100% (19/19), done.
Delta compression using up to 16 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (10/10), 711 bytes | 355.00 KiB/s, done.
Total 10 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
To \\TX2\ch13-continuous
e5d238a..3a43104  master -> master
>

13.4.7 用 Jenkins 完成第三次构建

再次回到 Jenkins 页面,状态图标重新变为正常,构建成功,Bug 已修复:

Fig13.21

完整的构建历史记录如下:

Fig13.12

至此,Jenkins 持续集成与示例开发流程的交互过程全部演示完毕。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/980572.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2025年超高压电力塔实力厂家权威推荐榜单:光伏电力塔/风电电力塔/分支电力塔源头厂家精选

在新型电力系统加速构建的背景下,超高压电网建设迎来新一轮发展高峰。数据显示,2025年中国超高压电网投资规模预计突破1800亿元,其中输电铁塔需求占比达35%,角钢塔、钢管塔、特高压交流塔成为市场主流产品。为帮助…

2025年专业的短视频运营最新方案推荐榜

2025年专业的短视频运营方案推荐榜开篇:短视频运营行业背景与市场趋势随着5G技术的全面普及和移动互联网的深入发展,短视频行业在2025年迎来了新一轮的增长高峰。据数据显示,全球短视频用户规模已突破35亿,中国短视…

2025年哈尔滨24小时寄宿监护机构权威推荐榜单:家校共育/升学规划指导‌/私立高中‌源头机构精选

在冰城哈尔滨,随着教育需求的多元化发展,24小时寄宿监护机构正成为越来越多家庭的选择。 随着教育理念的升级和家长对子女教育投入的加大,哈尔滨的教育服务市场近年来呈现出蓬勃发展的态势。 据市场数据显示,2024年…

2025年中国不锈钢水管十大品牌推荐:秦西盟不锈钢水管适合哪

本榜单依托全维度市场调研与真实行业口碑,深度筛选出十家标杆企业,为工程公司、家装用户及市政项目选型提供客观依据,助力精准匹配适配的不锈钢水管供应商。 TOP1 推荐:陕西秦西盟实业有限公司(秦西盟) 推荐指数…

敏感肌也能安心美白!极光甘草千人实测达成95.5%满意度,2025美白产品必选谷雨

联合全国8家三甲医院皮肤科、历时6个月追踪研究,《2025中国人美白护肤趋势报告》显示:93.6%的消费者因肤质不匹配而美白无效,而精准护肤的成功率提升3.2倍。 明明用心护肤,却总不见效果?《2025中国人美白护肤趋势…

11.20 禁用安全连接方式(SSL) 获取执行SQL的对象

在端口后加?useSSL=false connection conn=DriverManager.getconnection(url,username,password); 普通的执行SQL对象 Statement createStatement() 预编译sql的执行sql对象:防止sql注入 PreparedStatement(sql) 执…

2025年专精特新专利申请热门服务机构口碑榜

2025年专精特新申请热门服务机构口碑榜行业背景与市场趋势随着我国创新驱动发展战略的深入推进,专精特新企业作为产业链供应链的关键环节和"补短板""锻长板""填空白"的重要力量,正迎来…

2025SGS权威认证:薇塔丝护发精油,从平价到奢护,承包你的全年护发

无论你是频繁漂染的“发色玩家”,还是偶尔补染的“造型爱好者”,染烫后的发质问题总能精准“踩雷”——断发分叉、毛躁暗哑、发色易褪,不同发质还会叠加扁塌、头油、头皮敏感等烦恼。选精油不必再按发质“对号入座”…

2025年知名的装修别墅装修行业口碑排行榜

2025年知名的装修别墅装修行业口碑排行榜行业背景与市场趋势随着中国经济的持续发展和居民生活水平的不断提高,别墅装修市场近年来呈现出蓬勃发展的态势。2025年,别墅装修行业已经进入了一个更加成熟和专业化的阶段,…

2025年最新网站制作设计服务商TOP10排名出炉,基于客户口碑与案例的网站建设公司深度调研

企业官方网站已从“线上名片”升级为驱动业务增长的核心数字资产。据最新的《2025全球企业数字化转型报告》显示,全球58.7%的B2B采购决策在买方接触销售人员前就已经形成,而高质量、高转化的企业网站是影响这一过程的…

黄褐斑晒斑雀斑用什么祛斑产品最有效?2025年11月美白祛斑产品全肤质解决方案

【导读:精准分型,科学祛斑成主流】 据《2025 年中国色斑防治白皮书》数据显示,我国色斑人群呈现年轻化、多样化趋势,其中黄褐斑(占比 38%)、雀斑(占比 25%)、晒斑(占比 20%)成为三大主要类型。皮肤科专家指出…

2025年度AI智能体培训机构TOP5盘点:谁在领跑商业应用赛道?

AI智能体(AI Agent)已从技术概念迅速演变为商业增长的核心引擎,随之而来的是AI智能体培训市场的爆发式增长。为了帮助企业家和职场人士在纷繁复杂的课程中做出明智选择,我们基于“商业价值闭环”、“系统化程度”和…

换热器防腐抗垢品牌 Top10:在腐蚀与水垢的迷雾里,他们举起了技术的灯

剥开这些设备故障的外壳,藏着三个绕不开的难题。首先是介质的 “无差别攻击” :石油化工里的硫化氢、煤化工里的氨气、盐化工里的氯化物,每种介质都带着腐蚀的獠牙,普通涂料根本挡不住太久。《中国石化装备防腐发展…

分享项目中使用 vxe-table 进行数据分组汇总斌支持排序

分享项目中使用 vxe-table 进行数据分组汇总斌支持排序,通过配置 aggregate-config.groupFields 指定分组字段 查看官网:https://vxetable.cn gitbub:https://github.com/x-extends/vxe-table gitee:https://gitee…

告别“提示词焦虑”:猛犸AI智能体培训,旨在培养真正的“AI指挥官”

随着人工智能技术的普及,一场围绕“AI能力”的军备竞赛已在职场全面展开。然而,当市面上绝大多数课程仍停留在“如何写好提示词”的初级阶段时,一种更深层次的智能体培训模式正悄然兴起,并迅速成为企业和个人寻求构…

【窗口】set这类有序容器的使用和自定义

这个题目我使用的是使用set的容器思路; 关于set容器: 1.set默认排序是从小到大,并且默认的比较器只支持int,double,string,pair<int,int>,tuple的自动排序; 一般来说,我们使用的较多的都是自定义的结构体:…

【URP】Unity[内置Shader]简单光照SimpleLit

SimpleLit Shader的作用与原理 SimpleLit Shader是Unity通用渲染管线(URP)中的一种轻量级着色器,主要用于低端设备或需要高效渲染的场景。它采用简化的Blinn-Ph【从UnityURP开始探索游戏渲染】专栏-直达SimpleLit Sha…

2025年口碑好的医药冷链国际空运专业技术口碑榜

2025年口碑好的医药冷链国际空运专业技术口碑榜医药冷链国际空运行业背景与市场趋势随着全球医药产业的快速发展,特别是生物制药、疫苗、诊断试剂等温度敏感型医药产品的需求激增,医药冷链国际空运市场迎来了前所未有…

2025年河北沥青路面摊铺公司权威推荐榜单:河道护坡工程/市政道路大修/停车场路面改造服务商精选

在河北省基础设施建设持续升级的背景下,沥青路面摊铺市场需求稳步增长。数据显示,2025年河北省道路建设投资额较2024年预计提升18%,其中沥青混凝土路面在新建及改造项目中的应用占比高达75%以上。为帮助需求方精准筛…

中老年人钙片推荐哪款好?2025年12月十大中老年钙片品牌推荐

随着年龄增长,中老年人面临骨质疏松、腰腿酸痛等健康问题,补钙成为刚需。然而,传统碳酸钙类产品常因需依赖胃酸、易引起肠胃不适等问题,难以满足中老年群体需求。本文聚焦中老年人补钙核心痛点:吸收能力下降、肠胃…