4

继续继续,再整一个促销活动管理,文件导入导出都有了!

 2 years ago
source link: http://www.javaboy.org/2022/0428/springboot-sale.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

[TOC]

上篇文章中,我们搞定了渠道管理功能,这个相对来说比较简单。今天我们来看看促销活动的管理,在这个模块中,会有许多涉及到脚手架本身的修改,在这个过程中可以加深我们对这个脚手架的理解。

先来看看最终效果图吧:

20220426215908.png

这个页面上,你看到的所有功能按钮,均已实现。所以,就不废话了,开搞。

1. 数据库设计

数据库这里主要修改的地方有两处。

1.1 修改字典表

首先是修改字典表。在前端展示活动类型的时候,有两种不同的取值:

  • 年卡折扣券
  • 年卡代金券

像下面这样:

20220426220131.png

这里的活动类型下拉框我们当然可以直接在前端硬编码,但是既然用了这个脚手架,且这个脚手架又刚好提供了数据字典的功能,那么我们不妨将这两个选项加入到数据字典中,方便我们后面使用。

可以直接利用脚手架中的数据字典网页来添加,也可以直接在数据库表中来添加,我就省事一点,直接改表吧,修改两张表,分别是 sys_dict_typesys_dict_data 两张表,其中 sys_dict_type 中加的是字典类型,而 sys_dict_data 中加的则是字典的具体值,我添加的数据分别如下:

sys_dict_type

sys_dict_data

1.2 添加促销活动表

接下来就是活动促销表了,这个没啥好说的,直接开整就行了:

20220426221906.png

2. 创建新模块

2.1 新建模块

接下来创建一个专门写活动管理的新模块,有了前面写 channel 的经验,现在写 activity 不过是手到擒来的事。

新建一个名为 tienchin-activity 的模块,然后加入 common 依赖,如下图:

<description>
促销活动模块
</description>
<dependencies>
<!-- 通用工具-->
<dependency>
<groupId>org.javaboy</groupId>
<artifactId>tienchin-common</artifactId>
</dependency>
</dependencies>

当然这个新建的 activity 模块也拿去给 admin 模块依赖一下,将来在 admin 模块中调用 activity 模块的 service。

2.2 自动生成代码

MP 相关的依赖我们在上篇文章中已经配过了,这里咱就直接开始用就行了。

我们在 admin 模块的单元测试中新加一个方法,来用生成基础操作代码,如下:

@Test
public void activityGenerator() {
FastAutoGenerator.create("jdbc:mysql:///tienchin?serverTimezone=Asia/Shanghai&useSSL=false", "root", "123")
.globalConfig(builder -> {
builder.author("javaboy") // 设置作者
.disableOpenDir()
.fileOverride() // 覆盖已生成文件
.outputDir("/Users/sang/workspace/workspace02/tienchin/tienchin-activity/src/main/java"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("org.javaboy") // 设置父包名
.moduleName("activity") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.xml, "/Users/sang/workspace/workspace02/tienchin/tienchin-activity/src/main/resources/mapper/channel")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("tienchin_activity") // 设置需要生成的表名
.addTablePrefix("tienchin_");
})
.templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}

将自动生成的 controller 删除掉,我们将来重新写,最终生成的代码如下:

20220426223320.png

3. 服务端接口

接下来我们来看看服务端接口的开发。

我们在 admin 模块中,新建 ActivityController,来准备开发活动相关的接口。

3.1 常规 CRUD

首先是常规的 CRUD。

@RestController
@RequestMapping("/tienchin/activity")
public class ActivityController extends BaseController {

@Autowired
IActivityService activityService;

@PreAuthorize("@ss.hasPermi('tienchin:activity:add')")
@Log(title = "促销活动", businessType = BusinessType.INSERT)
@PostMapping("/")
public AjaxResult add(@Validated @RequestBody Activity activity) {
activity.setCreateBy(getUsername());
return toAjax(activityService.saveActivity(activity));
}

/**
* 状态修改
*/
@PreAuthorize("@ss.hasPermi('tienchin:activity:edit')")
@Log(title = "促销活动" , businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus")
public AjaxResult changeStatus(@RequestBody Activity activity) {
activity.setUpdateTime(LocalDateTime.now());
activity.setUpdateBy(getUsername());
return toAjax(activityService.updateById(activity));
}

@PreAuthorize("@ss.hasPermi('tienchin:activity:edit')")
@Log(title = "促销活动" , businessType = BusinessType.UPDATE)
@PutMapping("/")
public AjaxResult edit(@Validated @RequestBody Activity activity) {
activity.setUpdateBy(getUsername());
activity.setUpdateTime(LocalDateTime.now());
return toAjax(activityService.saveOrUpdate(activity));
}

@PreAuthorize("@ss.hasPermi('tienchin:activity:query')")
@GetMapping("/list")
public TableDataInfo getActivityList(Activity activity) {
startPage();
List<Activity> list = activityService.getActivityList(activity);
return getDataTable(list);
}

@PreAuthorize("@ss.hasPermi('tienchin:activity:remove')")
@Log(title = "促销活动" , businessType = BusinessType.DELETE)
@DeleteMapping("/{activityIds}")
public AjaxResult remove(@PathVariable Long[] activityIds) {
//待完善,将来加了其他功能后再继续完善
return toAjax(activityService.removeBatchByIds(Arrays.asList(activityIds)));
}

@PreAuthorize("@ss.hasPermi('tienchin:activity:query')")
@GetMapping(value = "/{id}")
public AjaxResult getActivityById(@PathVariable Long id) {
return AjaxResult.success(activityService.getById(id));
}
}

这些都是基础操作,其实也没啥好说的,大部分都用了 MP 自动生成的代码,自己几乎不需要写啥。

其中分页加条件查询的 /list 接口,是我自己写的,因为涉及到几个查询条件,该方法的定义如下:

public List<Activity> getActivityList(Activity activity) {
QueryWrapper<Activity> qw = new QueryWrapper<>();
if (activity.getChannel() != null) {
qw.lambda().eq(Activity::getChannel, activity.getChannel());
}
if (activity.getStatus() != null) {
qw.lambda().eq(Activity::getStatus, activity.getStatus());
}
if (activity.getEndTime() != null && activity.getBeginTime() != null) {
qw.lambda().ge(Activity::getBeginTime, activity.getBeginTime()).le(Activity::getEndTime, activity.getEndTime());
}
return list(qw);
}

用了 MP 的查询方法。涉及到一点点 Lambda,不过都很好懂,不熟悉 Lambda 的小伙伴可以在公众号后台回复 webflux,有相关教程。

另外这里还有一个小小细节,就是小伙伴们知道,从 JDK1.8 开始,推荐用 LocalDate 和 LocalDateTime,所以我这个项目涉及到时间的基本上都是用这两种类型,但是在原本的脚手架中,当涉及到对象和 JSON 的互转是,只支持对 Date 的转换,所以这块需要我自己手动处理下。

看了下,脚手架中相关的配置都放在 framework 中,具体位置在 org.javaboy.tienchin.framework.config,那么我的配置类就也写在这个位置吧,如下:

@Configuration
public class LocalDateTimeSerializerConfig {
private static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
private static final String DATE_PATTERN = "yyyy-MM-dd";

/**
* string转localdate
*/
@Bean
public Converter<String, LocalDate> localDateConverter() {
return new Converter<String, LocalDate>() {
@Override
public LocalDate convert(String source) {
if (source.trim().length() == 0) {
return null;
}
try {
return LocalDate.parse(source);
} catch (Exception e) {
return LocalDate.parse(source, DateTimeFormatter.ofPattern(DATE_PATTERN));
}
}
};
}

/**
* string转localdatetime
*/
@Bean
public Converter<String, LocalDateTime> localDateTimeConverter() {
return new Converter<String, LocalDateTime>() {
@Override
public LocalDateTime convert(String source) {
if (source.trim().length() == 0) {
return null;
}
// 先尝试ISO格式: 2019-07-15T16:00:00
try {
return LocalDateTime.parse(source);
} catch (Exception e) {
return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DATE_TIME_PATTERN));
}
}
};
}

/**
* 统一配置
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
JavaTimeModule module = new JavaTimeModule();
LocalDateTimeDeserializer localDateTimeDeserializer = new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer);
return builder -> {
builder.simpleDateFormat(DATE_TIME_PATTERN);
builder.serializers(new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_PATTERN)));
builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN)));
builder.modules(module);
};
}
}

配置类本身到也没啥说的,配了之后,将来项目中 LocalDate 转 JSON,就都是 yyyy-MM-dd 格式,LocalDateTime 转 JSON 就都是 yyyy-MM-dd HH:mm:ss 格式,反过来也一样。

3.2 导入导出

再来看看跟数据导入导出相关的几个接口。

首先 Excel 导入导出相关工具在脚手架中已经有了,我们直接用即可,需要做的准备工作,首先是在 Activity 实体类上加上相关注解,配置将来生成 Excel 时表格的 title,具体如下:

@TableName("tienchin_activity")
public class Activity implements Serializable {

/**
* id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;

/**
* 活动编号
*/
@Excel(name = "活动编号")
private String code;

/**
* 活动名称
*/
@Excel(name = "活动名称")
private String name;

/**
* 渠道来源
*/
@Excel(name = "渠道来源")
private String channel;

/**
* 活动简介
*/
@Excel(name = "活动简介")
private String info;

/**
* 活动类型
*/
@Excel(name = "活动类型",readConverterExp = "1=年费折扣卡,2=年费代金券")
private String type;

/**
* 年费折扣
*/
@Excel(name = "年费折扣")
private Float discount;

/**
* 年费代金券
*/
@Excel(name = "年费代金券")
private Double voucher;

/**
* 状态
*/
@Excel(name = "活动状态",readConverterExp = "0=正常,1=停用")
private String status;

/**
* 开始时间
*/
@Excel(name = "活动开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime beginTime;

/**
* 结束时间
*/
@Excel(name = "活动结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endTime;

private LocalDateTime createTime;

private LocalDateTime updateTime;

@Excel(name = "活动创建人")
private String updateBy;
@Excel(name = "活动修改人")
private String createBy;
}

没加 @Excel 注解的字段,也是将来导出 Excel 表格时不需要导出的字段。

这里有一个小问题,就是我的时间格式使用了 LocalDateTime,原本的脚手架在这块只支持 Date,LocalDateTime 的转换会有问题,为了支持 LocalDateTime,我这里修改了 org.javaboy.tienchin.common.utils.reflect.ReflectUtils#invokeMethodByName 方法,增加了对 LocalDateTime 的枚举,如下:

public static <E> E invokeMethodByName(final Object obj, final String methodName, final Object[] args) {
Method method = getAccessibleMethodByName(obj, methodName, args.length);
if (method == null) {
// 如果为空不报错,直接返回空。
logger.debug("在 [" + obj.getClass() + "] 中,没有找到 [" + methodName + "] 方法 ");
return null;
}
try {
// 类型转换(将参数数据类型转换为目标方法参数类型)
Class<?>[] cs = method.getParameterTypes();
for (int i = 0; i < cs.length; i++) {
if (args[i] != null && !args[i].getClass().equals(cs[i])) {
if (cs[i] == String.class) {
args[i] = Convert.toStr(args[i]);
if (StringUtils.endsWith((String) args[i], ".0")) {
args[i] = StringUtils.substringBefore((String) args[i], ".0");
}
} else if (cs[i] == Integer.class) {
args[i] = Convert.toInt(args[i]);
} else if (cs[i] == Long.class) {
args[i] = Convert.toLong(args[i]);
} else if (cs[i] == Double.class) {
args[i] = Convert.toDouble(args[i]);
} else if (cs[i] == Float.class) {
args[i] = Convert.toFloat(args[i]);
} else if (cs[i] == Date.class) {
if (args[i] instanceof String) {
args[i] = DateUtils.parseDate(args[i]);
} else {
args[i] = DateUtil.getJavaDate((Double) args[i]);
}
} else if (cs[i] == boolean.class || cs[i] == Boolean.class) {
args[i] = Convert.toBool(args[i]);
} else if (cs[i] == LocalDateTime.class) {
if (args[i] instanceof String) {
args[i] = DateUtils.getLocalDateTime((String) args[i]);
} else {
args[i] = DateUtils.getLocalDateTimeOfTimestamp((Long) args[i]);
}
}else if (cs[i] == LocalDate.class) {
if (args[i] instanceof String) {
args[i] = DateUtils.getLocalDate((String) args[i]);
} else {
args[i] = DateUtils.getLocalDateOfTimestamp((Long) args[i]);
}
}
}
}
return (E) method.invoke(obj, args);
} catch (Exception e) {
String msg = "method: " + method + ", obj: " + obj + ", args: " + args + "";
throw convertReflectionExceptionToUnchecked(msg, e);
}
}

这里涉及到了四个工具方法如下:

/**
* 时间戳转 LocalDateTime
*
* @param timestamp
* @return
*/
public static LocalDateTime getLocalDateTimeOfTimestamp(long timestamp) {
Instant instant = Instant.ofEpochMilli(timestamp);
ZoneId zone = ZoneId.systemDefault();
return LocalDateTime.ofInstant(instant, zone);
}
/**
* 时间戳转 LocalDate
*
* @param timestamp
* @return
*/
public static LocalDate getLocalDateOfTimestamp(long timestamp) {
Instant instant = Instant.ofEpochMilli(timestamp);
ZoneId zone = ZoneId.systemDefault();
return instant.atZone(zone).toLocalDate();
}
/**
* 字符串转 LocalDateTime
*
* @param datetime
* @return
*/
public static LocalDateTime getLocalDateTime(String datetime) {
return LocalDateTime.parse(datetime, DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS));
}
/**
* 字符串转 LocalDate
*
* @param date
* @return
*/
public static LocalDate getLocalDate(String date) {
return LocalDate.parse(date, DateTimeFormatter.ofPattern(YYYY_MM_DD));
}

好了,最后我们再提供三个导入导出相关的接口:

@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
ExcelUtil<Activity> util = new ExcelUtil<Activity>(Activity.class);
util.importTemplateExcel(response, "活动数据");
}
@Log(title = "促销活动" , businessType = BusinessType.EXPORT)
@PreAuthorize("@ss.hasPermi('tienchin:activity:export')")
@PostMapping("/export")
public void export(HttpServletResponse response, Activity activity) {
List<Activity> list = activityService.getActivityList(activity);
ExcelUtil<Activity> util = new ExcelUtil<Activity>(Activity.class);
util.exportExcel(response, list, "促销活动数据");
}
@Log(title = "促销活动" , businessType = BusinessType.IMPORT)
@PreAuthorize("@ss.hasPermi('tienchin:activity:import')")
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file) throws Exception {
ExcelUtil<Activity> util = new ExcelUtil<Activity>(Activity.class);
List<Activity> activityList = util.importExcel(file.getInputStream());
return toAjax(activityService.saveBatch(activityList));
}

这三个基本上也是照着用户接口写的,照猫画虎。

4. 前端页面开发

接下来开发前端页面。

4.1 请求接口

首先我们来开发请求接口,还是老规矩,新建一个 src/api/activity/index.js 文件,内容如下:

// 查询所有的活动信息
export function listActivity(query) {
return request({
url: '/tienchin/activity/list',
method: 'get',
params: query
})
}

// 根据 id 查询某一个活动的信息
export function getActivity(activityId) {
return request({
url: '/tienchin/activity/' + activityId,
method: 'get'
})
}

// 添加活动
export function addActivity(data) {
return request({
url: '/tienchin/activity/',
method: 'post',
data: data
})
}

// 更新活动信息
export function updateActivity(data) {
return request({
url: '/tienchin/activity/',
method: 'put',
data: data
})
}

// 更新活动状态
export function changeActivityStatus(id, status) {
const data = {
id,
status
}
return request({
url: '/tienchin/activity/changeStatus',
method: 'put',
data: data
})
}

// 根据 id 删除活动
export function delActivity(activityIds) {
return request({
url: '/tienchin/activity/' + activityIds,
method: 'delete'
})
}

这个基本上就是我们活动增删改查的所有信息了。对于文件导入导出是请求是单独封装的,一会直接在 .vue 文件中调用即可。

4.2 页面开发

具体的页面开发倒是不难,我们来看下最终的效果:

20220427213436.png

20220427213521.png

20220427213552.png

20220427213629.png

还有其他的我就不一一截图了。前端 vue 也不难,能做出 vhr 的小伙伴都能做出来这里的页面。没有特别直接说的地方,我也就不贴代码了。小伙伴们可以直接 GitHub 上下载源码查看。有不懂的地方欢迎留言讨论。

好啦,这次提交的功能是促销活动管理~小伙伴们赶紧去给个 star 呀,star 越多更的越快哈哈~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK