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

December 17, 2023
测试
测试
测试
测试
28 分钟阅读

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

先来看看最终效果图吧:

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

1. 数据库设计

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

1.1 修改字典表

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

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

像下面这样:

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

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

sys_dict_type

sys_dict_data

1.2 添加促销活动表

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

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 删除掉,我们将来重新写,最终生成的代码如下:

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 页面开发

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

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

5. 小结

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

  • https://github.com/lenve/tienchin

继续阅读

更多来自我们博客的帖子

如何安装 BuddyPress
由 测试 December 17, 2023
经过差不多一年的开发,BuddyPress 这个基于 WordPress Mu 的 SNS 插件正式版终于发布了。BuddyPress...
阅读更多
Filter如何工作
由 测试 December 17, 2023
在 web.xml...
阅读更多
如何理解CGAffineTransform
由 测试 December 17, 2023
CGAffineTransform A structure for holding an affine transformation matrix. ...
阅读更多