feat(跑店填报): 新增跑店填报表与接口

自主填报拆分为「驻店填报 / 跑店填报」两种类型,跑店填报的填报内容是
「巡店列表(多门店,每条含门店/定位/打卡照片)+ 活动小结」,与
yxe_self_report 的单门店结构不兼容:yxe_self_report 既没有多门店字段,
也没有区分驻店/跑店类型的字段。为不污染既有 928 条历史数据与既有
审核/导出逻辑,本次新建独立表 yxe_visit_report。

- entity/param/mapper(+xml)/service(+impl)/controller 全套
- 接口:page、audit/page、GET、userList、GET /{id}、POST、PUT、
  PUT /resubmit、DELETE /{id}、DELETE /batch、PUT /batch、POST /export
- 审核通过复用自主填报逻辑:scene=6 写余额,备注「跑店填报审核通过获得收入」
- 提交人须 auth_status=2;resubmit 仅本人 + 仅已驳回记录
- store_count 由 store_list JSON 长度重算,不信任前端传值
- 列表接口(page / audit/page)裁剪 store_list 与 image:
  一次巡店最多 9 图 × N 店,整列 JSON 可达数百 KB,列表只用 store_count,
  明细展开时再按 id 拉详情;userList(填报记录页要渲染门店列表)与导出不裁剪
- 导出表头含门店清单/连锁清单等从 store_list 展开的列

配套:
- yxe_visit_report.sql(建表,幂等,含执行前后核对 SELECT)
- yxe-mp  packageUser/self-report/select、visit
- yxe-admin yxeVisitReport API 与跑店详情
This commit is contained in:
2026-10-11 22:50:04 +08:00
parent 4914fb0088
commit eff7ba2340
8 changed files with 877 additions and 0 deletions
@@ -0,0 +1,395 @@
package com.gxwebsoft.yxe.controller;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.gxwebsoft.common.core.utils.ExcelExportUtil;
import com.gxwebsoft.common.core.web.ApiResult;
import com.gxwebsoft.common.core.web.BaseController;
import com.gxwebsoft.common.core.web.BatchParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.FileRecord;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.service.UserService;
import com.gxwebsoft.shop.entity.UserBalanceLog;
import com.gxwebsoft.shop.service.UserBalanceLogService;
import com.gxwebsoft.yxe.entity.YxeVisitReport;
import com.gxwebsoft.yxe.param.YxeVisitReportParam;
import com.gxwebsoft.yxe.service.YxeVisitReportService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 跑店填报控制器
*
* @author Codex
* @since 2026-10-11
*/
@Api(tags = "跑店填报管理")
@RestController
@RequestMapping("/api/yxe/yxe-visit-report")
public class YxeVisitReportController extends BaseController {
@Resource
private YxeVisitReportService yxeVisitReportService;
@Resource
private UserService userService;
@Resource
private ExcelExportUtil excelExportUtil;
@Resource
private UserBalanceLogService userBalanceLogService;
@ApiOperation("分页查询跑店填报")
@GetMapping("/page")
public ApiResult<PageResult<YxeVisitReport>> page(YxeVisitReportParam param) {
return success(trimListPayload(yxeVisitReportService.pageRel(param)));
}
@ApiOperation("审核页分页查询跑店填报")
@GetMapping("/audit/page")
public ApiResult<PageResult<YxeVisitReport>> auditPage(YxeVisitReportParam param) {
return success(trimListPayload(yxeVisitReportService.pageAuditSummary(param)));
}
@ApiOperation("查询全部跑店填报")
@GetMapping()
public ApiResult<List<YxeVisitReport>> list(YxeVisitReportParam param) {
return success(yxeVisitReportService.listRel(param));
}
@ApiOperation("查询当前用户跑店填报")
@GetMapping("/userList")
public ApiResult<List<YxeVisitReport>> userList(YxeVisitReportParam param) {
param.setUserId(getLoginUserId());
return success(yxeVisitReportService.listRel(param));
}
@ApiOperation("根据id查询跑店填报")
@GetMapping("/{id}")
public ApiResult<YxeVisitReport> get(@PathVariable("id") Integer id) {
return success(yxeVisitReportService.getByIdRel(id));
}
@ApiOperation("添加跑店填报")
@PostMapping()
public ApiResult<?> save(@RequestBody YxeVisitReport yxeVisitReport) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
if (!Integer.valueOf(2).equals(loginUser.getAuthStatus())) {
return fail("请先完成认证");
}
yxeVisitReport.setUserId(loginUser.getUserId());
yxeVisitReport.setStoreCount(countStoreItems(yxeVisitReport.getStoreList()));
if (yxeVisitReport.getStatus() == null) {
yxeVisitReport.setStatus(0);
}
if (yxeVisitReportService.save(yxeVisitReport)) {
return success("添加成功");
}
return fail("添加失败");
}
@ApiOperation("修改跑店填报")
@PutMapping()
public ApiResult<?> update(@RequestBody YxeVisitReport yxeVisitReport) {
YxeVisitReport current = yxeVisitReport.getId() == null ? null : yxeVisitReportService.getById(yxeVisitReport.getId());
boolean approving = current != null
&& Integer.valueOf(1).equals(yxeVisitReport.getStatus())
&& !Integer.valueOf(1).equals(current.getStatus());
if (approving) {
if (yxeVisitReport.getReward() == null) {
return fail("请填写奖金金额");
}
if (yxeVisitReport.getReward().compareTo(BigDecimal.ZERO) < 0) {
return fail("奖金金额不能小于0");
}
yxeVisitReport.setRejectReason("");
}
// 巡店明细只在提交时写一次;审核端不带该字段时不要覆盖成 null
if (yxeVisitReport.getStoreList() != null) {
yxeVisitReport.setStoreCount(countStoreItems(yxeVisitReport.getStoreList()));
}
if (yxeVisitReportService.updateById(yxeVisitReport)) {
if (approving) {
YxeVisitReport latest = yxeVisitReportService.getById(yxeVisitReport.getId());
User user = userService.getById(latest.getUserId());
BigDecimal reward = latest.getReward() == null ? BigDecimal.ZERO : latest.getReward();
UserBalanceLog userBalanceLog = new UserBalanceLog();
userBalanceLog.setUserId(latest.getUserId());
userBalanceLog.setOrderId(latest.getId());
userBalanceLog.setScene(6);
userBalanceLog.setMoney(reward);
userBalanceLog.setBalance(user.getBalance().add(reward));
userBalanceLog.setRemark("跑店填报审核通过获得收入");
userBalanceLogService.save(userBalanceLog);
user.setBalance(user.getBalance().add(reward));
userService.updateById(user);
}
return success("修改成功");
}
return fail("修改失败");
}
@ApiOperation("重新提交跑店填报")
@PutMapping("/resubmit")
public ApiResult<?> resubmit(@RequestBody YxeVisitReport yxeVisitReport) {
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("请先登录");
}
if (!Integer.valueOf(2).equals(loginUser.getAuthStatus())) {
return fail("请先完成认证");
}
if (yxeVisitReport == null || yxeVisitReport.getId() == null) {
return fail("缺少填报记录ID");
}
YxeVisitReport current = yxeVisitReportService.getById(yxeVisitReport.getId());
if (current == null) {
return fail("填报记录不存在");
}
// 只能重新提交自己的记录,避免复用通用修改接口改到他人数据
if (current.getUserId() == null || !current.getUserId().equals(loginUser.getUserId())) {
return fail("只能重新提交自己的填报记录");
}
// 仅驳回状态可重提,防止绕过审核直接改回待审核/已通过
if (!Integer.valueOf(2).equals(current.getStatus())) {
return fail("只有已驳回的填报记录可以重新提交");
}
YxeVisitReport update = new YxeVisitReport();
update.setId(current.getId());
update.setStoreList(yxeVisitReport.getStoreList());
update.setStoreCount(countStoreItems(yxeVisitReport.getStoreList()));
update.setImage(yxeVisitReport.getImage());
update.setSummary(yxeVisitReport.getSummary());
update.setLat(yxeVisitReport.getLat());
update.setLng(yxeVisitReport.getLng());
update.setLocation(yxeVisitReport.getLocation());
// 奖金金额与新增提交保持一致:由审核端确认,改动巡店明细后仍需同步上一轮金额
update.setReward(yxeVisitReport.getReward());
// 状态与驳回原因由服务端决定:回到待审核,并清掉上一轮的驳回原因
update.setStatus(0);
update.setRejectReason("");
if (yxeVisitReportService.updateById(update)) {
return success("重新提交成功");
}
return fail("重新提交失败");
}
@ApiOperation("删除跑店填报")
@DeleteMapping("/{id}")
public ApiResult<?> remove(@PathVariable("id") Integer id) {
if (yxeVisitReportService.removeById(id)) {
return success("删除成功");
}
return fail("删除失败");
}
@ApiOperation("批量修改跑店填报")
@PutMapping("/batch")
public ApiResult<?> updateBatch(@RequestBody BatchParam<YxeVisitReport> batchParam) {
if (batchParam.update(yxeVisitReportService, "id")) {
return success("修改成功");
}
return fail("修改失败");
}
@ApiOperation("批量删除跑店填报")
@DeleteMapping("/batch")
public ApiResult<?> removeBatch(@RequestBody List<Integer> ids) {
if (yxeVisitReportService.removeByIds(ids)) {
return success("删除成功");
}
return fail("删除失败");
}
@ApiOperation("导出跑店填报")
@PostMapping("/export")
public ApiResult<FileRecord> export(@RequestBody(required = false) YxeVisitReportParam param) throws IOException {
if (param == null) {
param = new YxeVisitReportParam();
}
List<YxeVisitReport> list = yxeVisitReportService.listRel(param);
List<String> headers = new ArrayList<>();
headers.add("ID");
headers.add("提交人ID");
headers.add("提交人姓名");
headers.add("提交人手机号");
headers.add("用户分组");
headers.add("巡店门店数");
headers.add("门店清单");
headers.add("连锁清单");
headers.add("活动小结");
headers.add("打卡时间");
headers.add("奖金金额");
headers.add("位置");
headers.add("状态");
headers.add("驳回原因");
headers.add("创建时间");
XSSFWorkbook workbook = excelExportUtil.createWorkbook("跑店填报", headers.toArray(new String[0]));
Sheet sheet = workbook.getSheetAt(0);
int rowNum = 1;
for (YxeVisitReport item : list) {
User user = userService.getById(item.getUserId());
List<Object> rowValues = new ArrayList<>();
rowValues.add(item.getId());
rowValues.add(item.getUserId());
rowValues.add(getUserName(user));
rowValues.add(getUserPhone(user));
rowValues.add(user == null ? "" : user.getUserGroup());
rowValues.add(item.getStoreCount());
rowValues.add(getStoreNamesText(item.getStoreList()));
rowValues.add(getChainNamesText(item.getStoreList()));
rowValues.add(getSummaryText(item.getSummary()));
rowValues.add(getVisitTimeText(item.getStoreList()));
rowValues.add(item.getReward());
rowValues.add(item.getLocation());
rowValues.add(getStatusText(item.getStatus()));
rowValues.add(item.getRejectReason());
rowValues.add(item.getCreateTime());
excelExportUtil.writeRow(sheet, rowNum++, rowValues.toArray());
}
excelExportUtil.autoSizeColumns(sheet, headers.size());
return success(excelExportUtil.saveWorkbook(workbook, "跑店填报数据", getLoginUserId()));
}
/**
* 列表接口不带 store_list:一次巡店最多 9 张图 × N 家门店,整列 JSON 可能有几百 KB,
* 列表页只用到 store_count,明细展开时再按 id 拉详情。
* 注意 userList(填报记录,前端要渲染门店列表)与 导出 不走这里。
*/
private PageResult<YxeVisitReport> trimListPayload(PageResult<YxeVisitReport> pageResult) {
if (pageResult == null || pageResult.getList() == null) {
return pageResult;
}
for (YxeVisitReport item : pageResult.getList()) {
if (item == null) {
continue;
}
item.setStoreList(null);
item.setImage(null);
}
return pageResult;
}
/**
* 巡店条数由 store_list JSON 长度决定,前端传值不可信,统一按内容重新计算
*/
private int countStoreItems(String storeList) {
return parseStoreList(storeList).size();
}
private JSONArray parseStoreList(String storeList) {
if (StrUtil.isBlank(storeList)) {
return new JSONArray();
}
try {
JSONArray parsed = JSON.parseArray(storeList);
return parsed == null ? new JSONArray() : parsed;
} catch (Exception ignored) {
return new JSONArray();
}
}
private String getStoreNamesText(String storeList) {
List<String> names = new ArrayList<>();
for (Object object : parseStoreList(storeList)) {
if (!(object instanceof JSONObject)) {
continue;
}
String name = ((JSONObject) object).getString("merchantName");
if (StrUtil.isNotBlank(name) && !names.contains(name.trim())) {
names.add(name.trim());
}
}
return String.join("、", names);
}
private String getChainNamesText(String storeList) {
List<String> names = new ArrayList<>();
for (Object object : parseStoreList(storeList)) {
if (!(object instanceof JSONObject)) {
continue;
}
String name = ((JSONObject) object).getString("chainName");
if (StrUtil.isNotBlank(name) && !names.contains(name.trim())) {
names.add(name.trim());
}
}
return String.join("、", names);
}
private String getVisitTimeText(String storeList) {
List<String> times = new ArrayList<>();
for (Object object : parseStoreList(storeList)) {
if (!(object instanceof JSONObject)) {
continue;
}
String time = ((JSONObject) object).getString("visitTime");
if (StrUtil.isNotBlank(time)) {
times.add(time.trim());
}
}
return String.join("、", times);
}
private String getSummaryText(String summary) {
if (StrUtil.isBlank(summary)) {
return "";
}
String text = summary.trim();
try {
Object parsed = JSON.parse(text);
if (parsed == null) {
return "";
}
if (parsed instanceof String) {
return ((String) parsed).trim();
}
return parsed.toString();
} catch (Exception ignored) {
return text;
}
}
private String getUserName(User user) {
if (user == null) {
return "";
}
if (user.getRealName() != null && !user.getRealName().trim().isEmpty()) {
return user.getRealName();
}
return user.getNickname();
}
private String getUserPhone(User user) {
return user == null ? "" : user.getPhone();
}
private String getStatusText(Integer status) {
if (status == null) {
return "";
}
switch (status) {
case 0:
return "待审核";
case 1:
return "通过";
case 2:
return "拒绝";
default:
return String.valueOf(status);
}
}
}
@@ -0,0 +1,88 @@
package com.gxwebsoft.yxe.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.gxwebsoft.common.system.entity.User;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 跑店填报
*
* @author Codex
* @since 2026-10-11
*/
@Data
@EqualsAndHashCode(callSuper = false)
@ApiModel(value = "YxeVisitReport对象", description = "跑店填报")
@TableName("yxe_visit_report")
public class YxeVisitReport implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty(value = "提交人ID")
private Integer userId;
@ApiModelProperty(value = "巡店门店数")
private Integer storeCount;
@ApiModelProperty(value = "巡店列表JSON(门店/定位/打卡照片)")
private String storeList;
@ApiModelProperty(value = "全部图片集合")
private String image;
@ApiModelProperty(value = "活动小结")
private String summary;
@ApiModelProperty(value = "奖金金额(审核时填写)")
private BigDecimal reward;
@ApiModelProperty(value = "纬度(整单,取首条门店)")
private String lat;
@ApiModelProperty(value = "经度(整单,取首条门店)")
private String lng;
@ApiModelProperty(value = "定位地址(整单,取首条门店)")
private String location;
@ApiModelProperty(value = "排序(数字越小越靠前)")
private Integer sortNumber;
@ApiModelProperty(value = "备注")
private String comments;
@ApiModelProperty(value = "状态, 0待审核, 1通过, 2拒绝")
private Integer status;
@ApiModelProperty(value = "驳回原因")
private String rejectReason;
@ApiModelProperty(value = "是否删除, 0否, 1是")
@TableLogic
private Integer deleted;
@ApiModelProperty(value = "租户id")
private Integer tenantId;
@ApiModelProperty(value = "创建时间")
private LocalDateTime createTime;
@ApiModelProperty(value = "修改时间")
private LocalDateTime updateTime;
@TableField(exist = false)
private User user;
}
@@ -0,0 +1,26 @@
package com.gxwebsoft.yxe.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.gxwebsoft.yxe.entity.YxeVisitReport;
import com.gxwebsoft.yxe.param.YxeVisitReportParam;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 跑店填报Mapper
*
* @author Codex
* @since 2026-10-11
*/
public interface YxeVisitReportMapper extends BaseMapper<YxeVisitReport> {
List<YxeVisitReport> selectPageRel(@Param("page") IPage<YxeVisitReport> page,
@Param("param") YxeVisitReportParam param);
List<YxeVisitReport> selectAuditPageRel(@Param("page") IPage<YxeVisitReport> page,
@Param("param") YxeVisitReportParam param);
List<YxeVisitReport> selectListRel(@Param("param") YxeVisitReportParam param);
}
@@ -0,0 +1,86 @@
<?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.yxe.mapper.YxeVisitReportMapper">
<!-- 门店明细在 store_list JSON 里,连锁/门店筛选只能按文本模糊匹配 -->
<sql id="selectSql">
SELECT a.*
FROM yxe_visit_report a
<where>
<if test="param.id != null">
AND a.id = #{param.id}
</if>
<if test="param.idList != null and param.idList != ''">
AND a.id IN
<foreach collection="param.idList.split(',')" item="item" separator="," open="(" close=")">
#{item}
</foreach>
</if>
<if test="param.userId != null">
AND a.user_id = #{param.userId}
</if>
<if test="param.userGroup != null and param.userGroup != ''">
AND a.user_id IN (SELECT user_id FROM sys_user WHERE deleted = 0 AND user_group LIKE CONCAT('%', #{param.userGroup}, '%'))
</if>
<if test="param.userIds != null and param.userIds.size() > 0">
AND a.user_id IN
<foreach collection="param.userIds" item="item" separator="," open="(" close=")">
#{item}
</foreach>
</if>
<if test="param.chainName != null and param.chainName != ''">
AND a.store_list LIKE CONCAT('%', #{param.chainName}, '%')
</if>
<if test="param.merchantName != null and param.merchantName != ''">
AND a.store_list LIKE CONCAT('%', #{param.merchantName}, '%')
</if>
<if test="param.dateStart != null and param.dateStart != ''">
AND a.create_time &gt;= CONCAT(#{param.dateStart}, ' 00:00:00')
</if>
<if test="param.dateEnd != null and param.dateEnd != ''">
AND a.create_time &lt;= CONCAT(#{param.dateEnd}, ' 23:59:59')
</if>
<if test="param.summary != null">
AND a.summary LIKE CONCAT('%', #{param.summary}, '%')
</if>
<if test="param.status != null">
AND a.status = #{param.status}
</if>
<if test="param.deleted != null">
AND a.deleted = #{param.deleted}
</if>
<if test="param.deleted == null">
AND a.deleted = 0
</if>
<if test="param.createTimeStart != null">
AND a.create_time &gt;= #{param.createTimeStart}
</if>
<if test="param.createTimeEnd != null">
AND a.create_time &lt;= #{param.createTimeEnd}
</if>
<if test="param.keywords != null">
AND (
a.store_list LIKE CONCAT('%', #{param.keywords}, '%')
OR a.summary LIKE CONCAT('%', #{param.keywords}, '%')
OR a.location LIKE CONCAT('%', #{param.keywords}, '%')
)
</if>
</where>
</sql>
<select id="selectPageRel" resultType="com.gxwebsoft.yxe.entity.YxeVisitReport">
<include refid="selectSql"></include>
ORDER BY a.create_time DESC
</select>
<select id="selectAuditPageRel" resultType="com.gxwebsoft.yxe.entity.YxeVisitReport">
<include refid="selectSql"></include>
ORDER BY a.create_time DESC
</select>
<select id="selectListRel" resultType="com.gxwebsoft.yxe.entity.YxeVisitReport">
<include refid="selectSql"></include>
ORDER BY a.create_time DESC
</select>
</mapper>
@@ -0,0 +1,69 @@
package com.gxwebsoft.yxe.param;
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.gxwebsoft.common.core.annotation.QueryField;
import com.gxwebsoft.common.core.annotation.QueryType;
import com.gxwebsoft.common.core.web.BaseParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Set;
/**
* 跑店填报查询参数
*
* @author Codex
* @since 2026-10-11
*/
@Data
@EqualsAndHashCode(callSuper = false)
@JsonInclude(JsonInclude.Include.NON_NULL)
@ApiModel(value = "YxeVisitReportParam对象", description = "跑店填报查询参数")
public class YxeVisitReportParam extends BaseParam {
private static final long serialVersionUID = 1L;
@QueryField(type = QueryType.EQ)
private Integer id;
@TableField(exist = false)
private String idList;
@QueryField(type = QueryType.EQ)
private Integer userId;
@ApiModelProperty(value = "用户分组")
private String userGroup;
@TableField(exist = false)
private Set<Integer> userIds;
private String chainName;
private String merchantName;
@TableField(exist = false)
private String dateStart;
@TableField(exist = false)
private String dateEnd;
private String summary;
@ApiModelProperty(value = "状态, 0待审核, 1通过, 2拒绝")
@QueryField(type = QueryType.EQ)
private Integer status;
@ApiModelProperty(value = "是否删除, 0否, 1是")
@QueryField(type = QueryType.EQ)
private Integer deleted;
@ApiModelProperty(value = "提交人姓名")
@TableField(exist = false)
private String realName;
@TableField(exist = false)
private Integer auditLite;
}
@@ -0,0 +1,25 @@
package com.gxwebsoft.yxe.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.yxe.entity.YxeVisitReport;
import com.gxwebsoft.yxe.param.YxeVisitReportParam;
import java.util.List;
/**
* 跑店填报Service
*
* @author Codex
* @since 2026-10-11
*/
public interface YxeVisitReportService extends IService<YxeVisitReport> {
PageResult<YxeVisitReport> pageRel(YxeVisitReportParam param);
PageResult<YxeVisitReport> pageAuditSummary(YxeVisitReportParam param);
List<YxeVisitReport> listRel(YxeVisitReportParam param);
YxeVisitReport getByIdRel(Integer id);
}
@@ -0,0 +1,127 @@
package com.gxwebsoft.yxe.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.gxwebsoft.common.core.web.PageParam;
import com.gxwebsoft.common.core.web.PageResult;
import com.gxwebsoft.common.system.entity.User;
import com.gxwebsoft.common.system.service.UserService;
import com.gxwebsoft.yxe.entity.YxeVisitReport;
import com.gxwebsoft.yxe.mapper.YxeVisitReportMapper;
import com.gxwebsoft.yxe.param.YxeVisitReportParam;
import com.gxwebsoft.yxe.service.YxeVisitReportService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 跑店填报Service实现
*
* @author Codex
* @since 2026-10-11
*/
@Service
public class YxeVisitReportServiceImpl extends ServiceImpl<YxeVisitReportMapper, YxeVisitReport> implements YxeVisitReportService {
@Resource
private UserService userService;
@Override
public PageResult<YxeVisitReport> pageRel(YxeVisitReportParam param) {
if (!handleRealNameFilter(param)) {
return new PageResult<>(Collections.emptyList(), 0L);
}
PageParam<YxeVisitReport, YxeVisitReportParam> page = new PageParam<>(param);
page.setDefaultOrder("sort_number asc, create_time desc");
List<YxeVisitReport> list = baseMapper.selectPageRel(page, param);
list.forEach(this::setData);
return new PageResult<>(list, page.getTotal());
}
@Override
public PageResult<YxeVisitReport> pageAuditSummary(YxeVisitReportParam param) {
if (!handleRealNameFilter(param)) {
return new PageResult<>(Collections.emptyList(), 0L);
}
PageParam<YxeVisitReport, YxeVisitReportParam> page = new PageParam<>(param);
page.setDefaultOrder("create_time desc");
List<YxeVisitReport> list = baseMapper.selectAuditPageRel(page, param);
// 审核摘要只要提交人身份,且不下发定位,避免列表接口携带大字段
list.forEach(item -> {
if (item == null) {
return;
}
if (item.getUserId() != null) {
User user = userService.getById(item.getUserId());
if (user != null) {
User liteUser = new User();
liteUser.setUserId(user.getUserId());
liteUser.setRealName(user.getRealName());
liteUser.setNickname(user.getNickname());
liteUser.setPhone(user.getPhone());
item.setUser(liteUser);
}
}
item.setLat(null);
item.setLng(null);
item.setComments(null);
});
return new PageResult<>(list, page.getTotal());
}
@Override
public List<YxeVisitReport> listRel(YxeVisitReportParam param) {
if (!handleRealNameFilter(param)) {
return Collections.emptyList();
}
List<YxeVisitReport> list = baseMapper.selectListRel(param);
list.forEach(this::setData);
PageParam<YxeVisitReport, YxeVisitReportParam> page = new PageParam<>();
page.setDefaultOrder("sort_number asc, create_time desc");
return page.sortRecords(list);
}
@Override
public YxeVisitReport getByIdRel(Integer id) {
YxeVisitReportParam param = new YxeVisitReportParam();
param.setId(id);
YxeVisitReport report = param.getOne(baseMapper.selectListRel(param));
if (report != null) {
setData(report);
}
return report;
}
private void setData(YxeVisitReport report) {
if (report == null) {
return;
}
if (report.getUserId() != null) {
report.setUser(userService.getById(report.getUserId()));
}
}
private boolean handleRealNameFilter(YxeVisitReportParam param) {
if (param == null || StrUtil.isBlank(param.getRealName())) {
return true;
}
List<User> users = userService.listByRealName(param.getRealName());
if (users == null || users.isEmpty()) {
return false;
}
Set<Integer> userIds = users.stream()
.map(User::getUserId)
.filter(userId -> userId != null && userId > 0)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (userIds.isEmpty()) {
return false;
}
param.setUserIds(userIds);
return true;
}
}
+61
View File
@@ -0,0 +1,61 @@
-- 用途:新增 yxe_visit_report 表(跑店填报)
-- 日期:2026-10-11
-- 背景:甲方要求把「自主填报」拆成「驻店填报 / 跑店填报」两种类型:
-- 1) 原自主填报页改为「驻店填报」,数据仍存 yxe_self_report,不动;
-- 2) 新增「跑店填报」,填报内容为「巡店列表(多门店,每条含门店/定位/打卡照片)+ 活动小结」。
-- 巡店列表是「一次填报多家门店」的数组结构,yxe_self_report 既没有多门店字段,
-- 也没有区分驻店/跑店类型的字段;本次选择新建独立表,避免污染既有 928 条历史数据
-- 与既有审核/导出逻辑。
-- 影响:新建空表,0 行业务数据;不改动 yxe_self_report 任何结构。
-- 性质:幂等,可重复执行(CREATE TABLE IF NOT EXISTS)。
-- 配套代码:
-- yxe-java YxeVisitReport.java / YxeVisitReportParam.java / YxeVisitReportMapper.java
-- / YxeVisitReportMapper.xml / YxeVisitReportService(+Impl)
-- / YxeVisitReportController.java
-- yxe-mp packageUser/self-report/select.vue(选择填报类型)
-- packageUser/self-report/visit.vue(跑店填报)
-- packageUser/self-report/records.vue(填报记录加类型切换)
-- pages/audit/index.vue(药审核加跑店填报类型)
-- yxe-admin src/api/yxe/yxeVisitReport/*、src/views/yxe/yxeSelfReport/*(类型筛选与跑店详情)
-- 执行前核对(应为 0,即表不存在)
SELECT COUNT(*) AS table_exists
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'yxe_visit_report';
-- 变更:建表(已存在则跳过)
CREATE TABLE IF NOT EXISTS `yxe_visit_report` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int DEFAULT NULL COMMENT '提交人ID',
`store_count` int DEFAULT 0 COMMENT '巡店门店数',
`store_list` longtext COMMENT '巡店列表JSON(门店/定位/打卡照片)',
`image` longtext COMMENT '全部图片集合',
`summary` longtext COMMENT '活动小结',
`reward` decimal(10,2) DEFAULT NULL COMMENT '奖金金额(审核时填写)',
`lat` varchar(64) DEFAULT NULL COMMENT '纬度(整单,取首条门店)',
`lng` varchar(64) DEFAULT NULL COMMENT '经度(整单,取首条门店)',
`location` varchar(500) DEFAULT NULL COMMENT '定位地址(整单,取首条门店)',
`sort_number` int DEFAULT 0 COMMENT '排序',
`comments` varchar(500) DEFAULT NULL COMMENT '备注',
`status` tinyint DEFAULT 0 COMMENT '状态:0待审核,1通过,2拒绝',
`reject_reason` varchar(500) DEFAULT NULL COMMENT '驳回原因',
`deleted` tinyint DEFAULT 0 COMMENT '逻辑删除',
`tenant_id` int DEFAULT 10325 COMMENT '租户ID',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_yxe_visit_report_user_id` (`user_id`),
KEY `idx_yxe_visit_report_status` (`status`),
KEY `idx_yxe_visit_report_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='跑店填报';
-- 执行后核对(应返回 1 行,且列/注释符合预期)
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_COMMENT, ORDINAL_POSITION
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'yxe_visit_report'
ORDER BY ORDINAL_POSITION;
-- 执行后核对(应为 0,跑店填报为新建功能,无历史数据)
SELECT COUNT(*) AS row_count FROM `yxe_visit_report`;