feat(charging): 实现扫码充电V2.0预约功能和VIN校验
- 新增站点预约时间配置字段,支持设置允许预约开始和结束时间
- 添加车辆VIN码存储和校验功能,在用户车辆信息中增加VIN字段
- 实现充电订单中预约时间和VIN信息记录,支持预期VIN和实际VIN对比
- 创建合作商绑定小程序用户关联表,支持合作商业务管理
- 开发站点收藏功能,用户可以收藏站点并设置降价提醒
- 实现收藏站点价格监控和微信订阅消息推送机制
- 添加VIN码识别和验证流程,确保充电车辆身份准确性
- 优化站点查询接口,增加VIN标识和预约相关字段返回
- 完善合作伙伴管理功能,支持绑定用户查询和管理操作
- 更新数据库表结构,增加预约、VIN、收藏相关数据表和字段
| | |
| | | private Integer isPartnerUser; |
| | | @ApiModelProperty("加盟商ID") |
| | | private Integer partnerId; |
| | | @ApiModelProperty("加盟商名称") |
| | | private String partnerName; |
| | | @ApiModelProperty("当前绑定的车辆") |
| | | private List<TAppUserCar> userCars; |
| | | } |
| | |
| | | package com.ruoyi.account.api.factory; |
| | | |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.api.dto.GiveVipDto; |
| | | import com.ruoyi.account.api.dto.SendMessageDTO; |
| | | import com.ruoyi.account.api.dto.UploadPdfDTO; |
| | |
| | | } |
| | | |
| | | @Override |
| | | public R<Page<TAppUser>> searchPartnerBindUsers(String phone, Integer pageNum, Integer pageSize) { |
| | | return R.fail("查询合作商可绑定用户失败:" + throwable.getMessage()); |
| | | } |
| | | |
| | | @Override |
| | | public R refundPoints(String param) { |
| | | return R.fail("后台兑换订单退款回退用户积分失败:"+throwable.getMessage()); |
| | | } |
| | |
| | | package com.ruoyi.account.api.feignClient; |
| | | |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.api.dto.GiveVipDto; |
| | | import com.ruoyi.account.api.dto.SendMessageDTO; |
| | | import com.ruoyi.account.api.dto.UploadPdfDTO; |
| | |
| | | */ |
| | | @GetMapping("/t-app-user/user/selectByPhoneLike") |
| | | R<List<TAppUser>> selectByPhoneLike(@RequestParam("phone") String phone); |
| | | |
| | | @GetMapping("/t-app-user/user/searchPartnerBindUsers") |
| | | R<Page<TAppUser>> searchPartnerBindUsers(@RequestParam(value = "phone", required = false) String phone, |
| | | @RequestParam("pageNum") Integer pageNum, |
| | | @RequestParam("pageSize") Integer pageSize); |
| | | /** |
| | | * 远程调用 积分兑换订单 退款回退积分 |
| | | * @return |
| | |
| | | @TableField("license_plate") |
| | | private String licensePlate; |
| | | |
| | | @ApiModelProperty(value = "车辆VIN码") |
| | | @TableField("vin") |
| | | private String vin; |
| | | |
| | | |
| | | @ApiModelProperty(value = "车牌") |
| | | @TableField("pic") |
| New file |
| | |
| | | package com.ruoyi.account.api.model; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import com.ruoyi.common.core.web.domain.BasePojo; |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | import lombok.EqualsAndHashCode; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = false) |
| | | @TableName("t_app_user_site_favorite") |
| | | @ApiModel(value = "用户收藏站点") |
| | | public class TAppUserSiteFavorite extends BasePojo { |
| | | |
| | | @TableId("id") |
| | | private Long id; |
| | | |
| | | @ApiModelProperty("小程序用户ID") |
| | | @TableField("app_user_id") |
| | | private Long appUserId; |
| | | |
| | | @ApiModelProperty("站点ID") |
| | | @TableField("site_id") |
| | | private Integer siteId; |
| | | |
| | | @ApiModelProperty("收藏时电价") |
| | | @TableField("favorite_price") |
| | | private BigDecimal favoritePrice; |
| | | |
| | | @ApiModelProperty("是否允许降价提醒") |
| | | @TableField("notify_enabled") |
| | | private Boolean notifyEnabled; |
| | | |
| | | @ApiModelProperty("最近一次已提醒价格") |
| | | @TableField("last_notified_price") |
| | | private BigDecimal lastNotifiedPrice; |
| | | } |
| | |
| | | public R<Partner> getPartnerById(Integer id) { |
| | | return R.fail("根据id获取合作商信息失败:" + throwable.getMessage()); |
| | | } |
| | | |
| | | @Override |
| | | public R<Partner> getPartnerByAppUserId(Long appUserId) { |
| | | return R.fail("根据小程序用户获取合作商信息失败:" + throwable.getMessage()); |
| | | } |
| | | |
| | | @Override |
| | | public R<Set<Integer>> authSite(Integer partnerId, String siteMenu) { |
| | |
| | | import com.ruoyi.chargingPile.api.model.Partner; |
| | | import com.ruoyi.chargingPile.api.model.Site; |
| | | import com.ruoyi.chargingPile.api.model.TChargingPile; |
| | | import com.ruoyi.chargingPile.api.vo.FavoriteSitePriceVO; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.web.page.PageInfo; |
| | | import org.slf4j.Logger; |
| | |
| | | public R<List<Site>> getSiteByIds(List<Integer> ids) { |
| | | return R.fail("根据id集合获取站点数据调用失败:" + throwable.getMessage()); |
| | | } |
| | | |
| | | @Override |
| | | public R<List<FavoriteSitePriceVO>> getFavoriteSitePrices(List<Integer> ids) { |
| | | return R.fail("获取收藏站点价格失败:" + throwable.getMessage()); |
| | | } |
| | | |
| | | @Override |
| | | public R<List<Site>> getSiteAll() { |
| | |
| | | */ |
| | | @PostMapping("/partner/getPartnerById/{id}") |
| | | R<Partner> getPartnerById(@PathVariable("id") Integer id); |
| | | |
| | | @PostMapping("/partner/getPartnerByAppUserId/{appUserId}") |
| | | R<Partner> getPartnerByAppUserId(@PathVariable("appUserId") Long appUserId); |
| | | |
| | | |
| | | /** |
| | |
| | | import com.ruoyi.chargingPile.api.model.Partner; |
| | | import com.ruoyi.chargingPile.api.model.Site; |
| | | import com.ruoyi.chargingPile.api.model.TChargingPile; |
| | | import com.ruoyi.chargingPile.api.vo.FavoriteSitePriceVO; |
| | | import com.ruoyi.common.core.constant.ServiceNameConstants; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.web.page.PageInfo; |
| | |
| | | */ |
| | | @PostMapping("/site/getSiteByIds") |
| | | R<List<Site>> getSiteByIds(@RequestBody List<Integer> ids); |
| | | |
| | | @PostMapping("/site/favoriteSitePrices") |
| | | R<List<FavoriteSitePriceVO>> getFavoriteSitePrices(@RequestBody List<Integer> ids); |
| | | |
| | | /** |
| | | * 获取所有站点 |
| | |
| | | @TableField("supportOrder") |
| | | @ApiModelProperty(value = "2.0修改字段-是否支持预约 0不支持1支持") |
| | | private Integer supportOrder; |
| | | @TableField("reservation_start_time") |
| | | @ApiModelProperty(value = "允许预约开始时间,格式HH:mm") |
| | | private String reservationStartTime; |
| | | @TableField("reservation_end_time") |
| | | @ApiModelProperty(value = "允许预约结束时间,格式HH:mm") |
| | | private String reservationEndTime; |
| | | /** |
| | | * 高速路服务区编号 |
| | | */ |
| New file |
| | |
| | | package com.ruoyi.chargingPile.api.model; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.FieldFill; |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.time.LocalDateTime; |
| | | |
| | | /** |
| | | * 合作商与小程序用户绑定关系。 |
| | | */ |
| | | @Data |
| | | @TableName("t_partner_app_user") |
| | | @ApiModel(value = "TPartnerAppUser对象", description = "合作商与小程序用户绑定关系") |
| | | public class TPartnerAppUser implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | @TableId(value = "id", type = IdType.AUTO) |
| | | @ApiModelProperty("主键") |
| | | private Long id; |
| | | |
| | | @TableField("partner_id") |
| | | @ApiModelProperty("合作商ID") |
| | | private Integer partnerId; |
| | | |
| | | @TableField("app_user_id") |
| | | @ApiModelProperty("小程序用户ID") |
| | | private Long appUserId; |
| | | |
| | | @TableField(value = "create_time", fill = FieldFill.INSERT) |
| | | @ApiModelProperty("创建时间") |
| | | private LocalDateTime createTime; |
| | | } |
| New file |
| | |
| | | package com.ruoyi.chargingPile.api.vo; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | @Data |
| | | public class FavoriteSitePriceVO { |
| | | private Integer siteId; |
| | | private String siteName; |
| | | private Integer status; |
| | | private BigDecimal currentPrice; |
| | | } |
| | |
| | | @ApiModelProperty(value = "充电模式(1=超级快充,2=快充,3=慢充") |
| | | private Integer chargeMode; |
| | | |
| | | @ApiModelProperty(value = "是否支持预约") |
| | | private Integer supportOrder; |
| | | @ApiModelProperty(value = "允许预约开始时间,格式HH:mm") |
| | | private String reservationStartTime; |
| | | @ApiModelProperty(value = "允许预约结束时间,格式HH:mm") |
| | | private String reservationEndTime; |
| | | @ApiModelProperty(value = "是否支持VIN码识别") |
| | | private Integer vinFlag; |
| | | |
| | | @ApiModelProperty(value = "上次选择车辆logo") |
| | | private String carLogo; |
| | | @ApiModelProperty(value = "上次选择车辆车牌号") |
| | | private String licensePlate; |
| | | @ApiModelProperty(value = "上次选择车辆VIN") |
| | | private String vin; |
| | | @ApiModelProperty(value = "上次选择车辆品牌") |
| | | private String vehicleBrand; |
| | | @ApiModelProperty(value = "上次选择车辆id") |
| | |
| | | private Integer secure_connection; // 车枪连接(0=未连接,1=连接) |
| | | private Integer nsulation_detection; // 绝缘检测(0=安全,1=不安全) |
| | | private Integer electronic_lock_lock; // 电磁锁状态(0=未锁,1=已锁) |
| | | private String vin; // 当前车辆VIN码 |
| | | |
| | | } |
| | |
| | | @TableField("plate_num") |
| | | private String plateNum; |
| | | |
| | | @ApiModelProperty(value = "预约充电时间") |
| | | @TableField("reservation_time") |
| | | private LocalDateTime reservationTime; |
| | | |
| | | @ApiModelProperty(value = "支付车辆VIN快照") |
| | | @TableField("expected_vin") |
| | | private String expectedVin; |
| | | |
| | | @ApiModelProperty(value = "启动时设备识别VIN") |
| | | @TableField("actual_vin") |
| | | private String actualVin; |
| | | |
| | | @ApiModelProperty(value = "站点 id") |
| | | @TableField("site_id") |
| | | private Integer siteId; |
| | |
| | | private Integer secure_connection; // 车枪连接(0=未连接,1=连接) |
| | | private Integer nsulation_detection; // 绝缘检测(0=安全,1=不安全) |
| | | private Integer electronic_lock_lock; // 电磁锁状态(0=未锁,1=已锁) |
| | | private String vin; // 当前车辆VIN码 |
| | | |
| | | } |
| | |
| | | .orderByDesc("create_time")); |
| | | if (!cars.isEmpty()){ |
| | | data.setLicensePlate(cars.get(0).getLicensePlate()); |
| | | data.setVin(cars.get(0).getVin()); |
| | | data.setVehicleBrand(cars.get(0).getVehicleBrand()); |
| | | data.setId(cars.get(0).getId().toString()); |
| | | } |
| | |
| | | import com.ruoyi.account.util.*; |
| | | import com.ruoyi.account.util.carBrand.CarBrandUtil; |
| | | import com.ruoyi.account.wx.model.WeixinProperties; |
| | | import com.ruoyi.chargingPile.api.feignClient.PartnerClient; |
| | | import com.ruoyi.chargingPile.api.model.Partner; |
| | | import com.ruoyi.common.core.constant.Constants; |
| | | import com.ruoyi.common.core.constant.MsgConstants; |
| | | import com.ruoyi.common.core.domain.R; |
| | |
| | | private GiveVipUtil giveVipUtil; |
| | | @Resource |
| | | private TAppUserWhitelistService appUserWhitelistService; |
| | | @Resource |
| | | private PartnerClient partnerClient; |
| | | |
| | | @Autowired |
| | | private RedissonClient redissonClient; |
| | |
| | | appUserInfoDto.setFirstAdd(byId.getFirstAdd()!=null?byId.getFirstAdd():0); |
| | | appUserInfoDto.setPoints(byId.getPoints()==null?0:byId.getPoints()); |
| | | appUserInfoDto.setBalance(byId.getBalance()==null?BigDecimal.ZERO:byId.getBalance()); |
| | | TAppUserWhitelist partnerWhitelist = appUserWhitelistService.lambdaQuery() |
| | | .eq(TAppUserWhitelist::getAppUserId, userId) |
| | | .eq(TAppUserWhitelist::getScopeType, 3) |
| | | .eq(TAppUserWhitelist::getStatus, 1) |
| | | .eq(TAppUserWhitelist::getDelFlag, false) |
| | | .isNotNull(TAppUserWhitelist::getPartnerId) |
| | | .last("limit 1") |
| | | .one(); |
| | | appUserInfoDto.setIsPartnerUser(partnerWhitelist == null ? 0 : 1); |
| | | appUserInfoDto.setPartnerId(partnerWhitelist == null ? null : partnerWhitelist.getPartnerId()); |
| | | Partner boundPartner = null; |
| | | try { |
| | | R<Partner> partnerResult = partnerClient.getPartnerByAppUserId(userId); |
| | | boundPartner = partnerResult == null ? null : partnerResult.getData(); |
| | | } catch (Exception e) { |
| | | log.warn("查询小程序用户绑定合作商失败,继续兼容旧白名单关系: {}", e.getMessage()); |
| | | } |
| | | |
| | | Integer partnerId = boundPartner == null ? null : boundPartner.getId(); |
| | | String partnerName = boundPartner == null ? null : boundPartner.getName(); |
| | | if (partnerId == null) { |
| | | TAppUserWhitelist partnerWhitelist = appUserWhitelistService.lambdaQuery() |
| | | .eq(TAppUserWhitelist::getAppUserId, userId) |
| | | .eq(TAppUserWhitelist::getScopeType, 3) |
| | | .eq(TAppUserWhitelist::getStatus, 1) |
| | | .eq(TAppUserWhitelist::getDelFlag, false) |
| | | .isNotNull(TAppUserWhitelist::getPartnerId) |
| | | .last("limit 1") |
| | | .one(); |
| | | if (partnerWhitelist != null) { |
| | | partnerId = partnerWhitelist.getPartnerId(); |
| | | R<Partner> legacyPartnerResult = partnerClient.getPartnerById(partnerId); |
| | | Partner legacyPartner = legacyPartnerResult == null ? null : legacyPartnerResult.getData(); |
| | | partnerName = legacyPartner == null ? null : legacyPartner.getName(); |
| | | } |
| | | } |
| | | appUserInfoDto.setIsPartnerUser(partnerId == null ? 0 : 1); |
| | | appUserInfoDto.setPartnerId(partnerId); |
| | | appUserInfoDto.setPartnerName(partnerName); |
| | | return R.ok(appUserInfoDto); |
| | | } |
| | | |
| | |
| | | return R.ok(appUserService.list(Wrappers.lambdaQuery(TAppUser.class) |
| | | .eq(TAppUser::getPhone,phone))); |
| | | } |
| | | |
| | | @GetMapping("/user/searchPartnerBindUsers") |
| | | public R<Page<TAppUser>> searchPartnerBindUsers(@RequestParam(value = "phone", required = false) String phone, |
| | | @RequestParam(defaultValue = "1") Integer pageNum, |
| | | @RequestParam(defaultValue = "10") Integer pageSize) { |
| | | Page<TAppUser> page = appUserService.lambdaQuery() |
| | | .like(StringUtils.isNotEmpty(phone), TAppUser::getPhone, phone) |
| | | .orderByDesc(BasePojo::getCreateTime) |
| | | .page(Page.of(pageNum, pageSize)); |
| | | page.getRecords().forEach(user -> user.setUid(user.getId().toString())); |
| | | return R.ok(page); |
| | | } |
| | | |
| | | @Log(title = "【我的】签到", businessType = BusinessType.INSERT,operatorType = OperatorType.MOBILE) |
| | | @ApiOperation(value = "签到", tags = {"小程序-个人中心-签到"}) |
| New file |
| | |
| | | package com.ruoyi.account.controller; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.ruoyi.account.api.model.TAppUserSiteFavorite; |
| | | import com.ruoyi.account.dto.FavoriteSiteRequest; |
| | | import com.ruoyi.account.service.TAppUserSiteFavoriteService; |
| | | import com.ruoyi.chargingPile.api.feignClient.SiteClient; |
| | | import com.ruoyi.chargingPile.api.vo.FavoriteSitePriceVO; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.web.domain.AjaxResult; |
| | | import com.ruoyi.common.security.service.TokenService; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | @RestController |
| | | @RequestMapping("/site-favorite") |
| | | public class TAppUserSiteFavoriteController { |
| | | |
| | | @Resource |
| | | private TAppUserSiteFavoriteService favoriteService; |
| | | |
| | | @Resource |
| | | private TokenService tokenService; |
| | | |
| | | @Resource |
| | | private SiteClient siteClient; |
| | | |
| | | @Value("${wx.applet.price-drop-template-id:}") |
| | | private String priceDropTemplateId; |
| | | |
| | | @GetMapping("/list") |
| | | @ApiOperation(value = "查询当前用户收藏站点", tags = {"小程序-收藏"}) |
| | | public AjaxResult<List<TAppUserSiteFavorite>> list() { |
| | | Long userId = tokenService.getLoginUserApplet().getUserId(); |
| | | return AjaxResult.success(favoriteService.list(new LambdaQueryWrapper<TAppUserSiteFavorite>() |
| | | .eq(TAppUserSiteFavorite::getAppUserId, userId) |
| | | .orderByDesc(TAppUserSiteFavorite::getCreateTime))); |
| | | } |
| | | |
| | | @GetMapping("/check/{siteId}") |
| | | @ApiOperation(value = "检查站点是否已收藏", tags = {"小程序-收藏"}) |
| | | public AjaxResult<Boolean> check(@PathVariable Integer siteId) { |
| | | Long userId = tokenService.getLoginUserApplet().getUserId(); |
| | | return AjaxResult.success(favoriteService.count(new LambdaQueryWrapper<TAppUserSiteFavorite>() |
| | | .eq(TAppUserSiteFavorite::getAppUserId, userId) |
| | | .eq(TAppUserSiteFavorite::getSiteId, siteId)) > 0); |
| | | } |
| | | |
| | | @PostMapping |
| | | @ApiOperation(value = "收藏站点", tags = {"小程序-收藏"}) |
| | | public AjaxResult<Void> save(@Validated @RequestBody FavoriteSiteRequest request) { |
| | | Long userId = tokenService.getLoginUserApplet().getUserId(); |
| | | R<List<FavoriteSitePriceVO>> priceResponse = siteClient.getFavoriteSitePrices( |
| | | Collections.singletonList(request.getSiteId())); |
| | | if (priceResponse == null || priceResponse.getData() == null || priceResponse.getData().isEmpty() |
| | | || priceResponse.getData().get(0).getCurrentPrice() == null) { |
| | | return AjaxResult.error("站点当前价格不可用,暂时无法收藏"); |
| | | } |
| | | favoriteService.saveOrRestore(userId, request.getSiteId(), |
| | | priceResponse.getData().get(0).getCurrentPrice(), request.getNotifyEnabled()); |
| | | return AjaxResult.success(); |
| | | } |
| | | |
| | | @DeleteMapping("/{siteId}") |
| | | @ApiOperation(value = "取消收藏站点", tags = {"小程序-收藏"}) |
| | | public AjaxResult<Void> delete(@PathVariable Integer siteId) { |
| | | Long userId = tokenService.getLoginUserApplet().getUserId(); |
| | | favoriteService.remove(new LambdaQueryWrapper<TAppUserSiteFavorite>() |
| | | .eq(TAppUserSiteFavorite::getAppUserId, userId) |
| | | .eq(TAppUserSiteFavorite::getSiteId, siteId)); |
| | | return AjaxResult.success(); |
| | | } |
| | | |
| | | @PostMapping("/{siteId}/subscribe") |
| | | @ApiOperation(value = "开启站点降价提醒", tags = {"小程序-收藏"}) |
| | | public AjaxResult<Void> subscribe(@PathVariable Integer siteId) { |
| | | Long userId = tokenService.getLoginUserApplet().getUserId(); |
| | | favoriteService.lambdaUpdate() |
| | | .eq(TAppUserSiteFavorite::getAppUserId, userId) |
| | | .eq(TAppUserSiteFavorite::getSiteId, siteId) |
| | | .set(TAppUserSiteFavorite::getNotifyEnabled, true) |
| | | .update(); |
| | | return AjaxResult.success(); |
| | | } |
| | | |
| | | @GetMapping("/template") |
| | | @ApiOperation(value = "获取降价提醒模板ID", tags = {"小程序-收藏"}) |
| | | public AjaxResult<String> template() { |
| | | return AjaxResult.success(priceDropTemplateId); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.account.dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import javax.validation.constraints.NotNull; |
| | | import java.math.BigDecimal; |
| | | |
| | | @Data |
| | | @ApiModel("收藏站点请求") |
| | | public class FavoriteSiteRequest { |
| | | |
| | | @NotNull(message = "站点ID不能为空") |
| | | @ApiModelProperty("站点ID") |
| | | private Integer siteId; |
| | | |
| | | @NotNull(message = "收藏价格不能为空") |
| | | @ApiModelProperty("收藏时电价") |
| | | private BigDecimal favoritePrice; |
| | | |
| | | @ApiModelProperty("是否允许降价提醒") |
| | | private Boolean notifyEnabled; |
| | | } |
| New file |
| | |
| | | package com.ruoyi.account.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.ruoyi.account.api.model.TAppUserSiteFavorite; |
| | | import org.apache.ibatis.annotations.Insert; |
| | | import org.apache.ibatis.annotations.Param; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | public interface TAppUserSiteFavoriteMapper extends BaseMapper<TAppUserSiteFavorite> { |
| | | |
| | | @Insert("INSERT INTO t_app_user_site_favorite " + |
| | | "(app_user_id, site_id, favorite_price, notify_enabled, create_time, del_flag) " + |
| | | "VALUES (#{appUserId}, #{siteId}, #{favoritePrice}, #{notifyEnabled}, NOW(), 0) " + |
| | | "ON DUPLICATE KEY UPDATE favorite_price = VALUES(favorite_price), " + |
| | | "notify_enabled = VALUES(notify_enabled), last_notified_price = NULL, " + |
| | | "create_time = NOW(), del_flag = 0") |
| | | int upsert(@Param("appUserId") Long appUserId, |
| | | @Param("siteId") Integer siteId, |
| | | @Param("favoritePrice") BigDecimal favoritePrice, |
| | | @Param("notifyEnabled") Boolean notifyEnabled); |
| | | } |
| New file |
| | |
| | | package com.ruoyi.account.service; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import com.ruoyi.account.api.model.TAppUserSiteFavorite; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | public interface TAppUserSiteFavoriteService extends IService<TAppUserSiteFavorite> { |
| | | |
| | | void saveOrRestore(Long appUserId, Integer siteId, BigDecimal favoritePrice, Boolean notifyEnabled); |
| | | } |
| New file |
| | |
| | | package com.ruoyi.account.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.ruoyi.account.api.model.TAppUserSiteFavorite; |
| | | import com.ruoyi.account.mapper.TAppUserSiteFavoriteMapper; |
| | | import com.ruoyi.account.service.TAppUserSiteFavoriteService; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.math.BigDecimal; |
| | | |
| | | @Service |
| | | public class TAppUserSiteFavoriteServiceImpl |
| | | extends ServiceImpl<TAppUserSiteFavoriteMapper, TAppUserSiteFavorite> |
| | | implements TAppUserSiteFavoriteService { |
| | | |
| | | @Override |
| | | public void saveOrRestore(Long appUserId, Integer siteId, BigDecimal favoritePrice, Boolean notifyEnabled) { |
| | | baseMapper.upsert(appUserId, siteId, favoritePrice, Boolean.TRUE.equals(notifyEnabled)); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ruoyi.account.task; |
| | | |
| | | import com.alibaba.fastjson2.JSON; |
| | | import com.alibaba.fastjson2.JSONObject; |
| | | import com.ruoyi.account.api.model.TAppUser; |
| | | import com.ruoyi.account.api.model.TAppUserSiteFavorite; |
| | | import com.ruoyi.account.service.TAppUserService; |
| | | import com.ruoyi.account.service.TAppUserSiteFavoriteService; |
| | | import com.ruoyi.account.wx.model.WeixinProperties; |
| | | import com.ruoyi.account.wx.tools.WxAppletTools; |
| | | import com.ruoyi.chargingPile.api.feignClient.SiteClient; |
| | | import com.ruoyi.chargingPile.api.vo.FavoriteSitePriceVO; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.utils.HttpUtils; |
| | | import com.ruoyi.common.redis.service.RedisService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.redisson.api.RLock; |
| | | import org.redisson.api.RedissonClient; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.scheduling.annotation.Scheduled; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.util.StringUtils; |
| | | import org.springframework.web.client.RestTemplate; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.math.BigDecimal; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.function.Function; |
| | | import java.util.stream.Collectors; |
| | | |
| | | @Slf4j |
| | | @Component |
| | | public class FavoritePriceDropTask { |
| | | |
| | | private static final String SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token="; |
| | | |
| | | @Resource |
| | | private TAppUserSiteFavoriteService favoriteService; |
| | | @Resource |
| | | private TAppUserService appUserService; |
| | | @Resource |
| | | private SiteClient siteClient; |
| | | @Resource |
| | | private RestTemplate wxRestTemplate; |
| | | @Resource |
| | | private WeixinProperties wxConfig; |
| | | @Resource |
| | | private RedisService redisService; |
| | | @Resource |
| | | private RedissonClient redissonClient; |
| | | |
| | | @Value("${wx.applet.price-drop-template-id:}") |
| | | private String templateId; |
| | | |
| | | @Value("${wx.applet.price-drop-page:pagesMine/myCollection/myCollection}") |
| | | private String page; |
| | | |
| | | @Scheduled(fixedDelay = 30 * 60 * 1000L, initialDelay = 2 * 60 * 1000L) |
| | | public void notifyPriceDrop() { |
| | | if (!StringUtils.hasText(templateId)) { |
| | | return; |
| | | } |
| | | RLock lock = redissonClient.getLock("FAVORITE_SITE_PRICE_DROP_TASK"); |
| | | boolean locked = false; |
| | | try { |
| | | locked = lock.tryLock(0, 10, TimeUnit.MINUTES); |
| | | if (!locked) { |
| | | return; |
| | | } |
| | | List<TAppUserSiteFavorite> favorites = favoriteService.lambdaQuery() |
| | | .eq(TAppUserSiteFavorite::getNotifyEnabled, true) |
| | | .list(); |
| | | if (favorites.isEmpty()) { |
| | | return; |
| | | } |
| | | List<Integer> siteIds = favorites.stream().map(TAppUserSiteFavorite::getSiteId) |
| | | .filter(Objects::nonNull).distinct().collect(Collectors.toList()); |
| | | R<List<FavoriteSitePriceVO>> response = siteClient.getFavoriteSitePrices(siteIds); |
| | | List<FavoriteSitePriceVO> prices = response == null || response.getData() == null |
| | | ? Collections.emptyList() : response.getData(); |
| | | Map<Integer, FavoriteSitePriceVO> priceMap = prices.stream() |
| | | .collect(Collectors.toMap(FavoriteSitePriceVO::getSiteId, Function.identity(), (a, b) -> a)); |
| | | for (TAppUserSiteFavorite favorite : favorites) { |
| | | FavoriteSitePriceVO price = priceMap.get(favorite.getSiteId()); |
| | | if (price == null || price.getCurrentPrice() == null || favorite.getFavoritePrice() == null |
| | | || price.getCurrentPrice().compareTo(favorite.getFavoritePrice()) >= 0) { |
| | | continue; |
| | | } |
| | | TAppUser user = appUserService.getById(favorite.getAppUserId()); |
| | | if (user == null || !StringUtils.hasText(user.getWxOpenid())) { |
| | | continue; |
| | | } |
| | | if (sendSubscribeMessage(user.getWxOpenid(), favorite, price)) { |
| | | favorite.setNotifyEnabled(false); |
| | | favorite.setLastNotifiedPrice(price.getCurrentPrice()); |
| | | favoriteService.updateById(favorite); |
| | | } |
| | | } |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | } catch (Exception e) { |
| | | log.error("收藏站点降价提醒任务执行失败", e); |
| | | } finally { |
| | | if (locked && lock.isHeldByCurrentThread()) { |
| | | lock.unlock(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private boolean sendSubscribeMessage(String openId, TAppUserSiteFavorite favorite, FavoriteSitePriceVO price) { |
| | | BigDecimal decrease = favorite.getFavoritePrice().subtract(price.getCurrentPrice()); |
| | | JSONObject body = new JSONObject(); |
| | | body.put("touser", openId); |
| | | body.put("template_id", templateId); |
| | | body.put("page", page + "?siteId=" + favorite.getSiteId()); |
| | | JSONObject data = new JSONObject(); |
| | | data.put("thing1", value(limit(price.getSiteName(), 20))); |
| | | data.put("amount2", value(favorite.getFavoritePrice().setScale(4, BigDecimal.ROUND_HALF_UP).toPlainString())); |
| | | data.put("amount3", value(price.getCurrentPrice().setScale(4, BigDecimal.ROUND_HALF_UP).toPlainString())); |
| | | data.put("amount4", value(decrease.setScale(4, BigDecimal.ROUND_HALF_UP).toPlainString())); |
| | | body.put("data", data); |
| | | String accessToken = new WxAppletTools(wxRestTemplate, wxConfig, redisService).getAccessToken(""); |
| | | String result = HttpUtils.post(SEND_URL + accessToken, body.toJSONString()); |
| | | JSONObject response = JSON.parseObject(result); |
| | | boolean success = response != null && response.getIntValue("errcode") == 0; |
| | | if (!success) { |
| | | log.warn("发送收藏站点降价提醒失败,userId={}, siteId={}, response={}", |
| | | favorite.getAppUserId(), favorite.getSiteId(), result); |
| | | } |
| | | return success; |
| | | } |
| | | |
| | | private JSONObject value(String value) { |
| | | JSONObject item = new JSONObject(); |
| | | item.put("value", value); |
| | | return item; |
| | | } |
| | | |
| | | private String limit(String value, int length) { |
| | | if (!StringUtils.hasText(value) || value.length() <= length) { |
| | | return value; |
| | | } |
| | | return value.substring(0, length); |
| | | } |
| | | } |
| | |
| | | <id column="id" property="id" /> |
| | | <result column="app_user_id" property="appUserId" /> |
| | | <result column="license_plate" property="licensePlate" /> |
| | | <result column="vin" property="vin" /> |
| | | <result column="vehicle_brand" property="vehicleBrand" /> |
| | | <result column="vehicle_model" property="vehicleModel" /> |
| | | <result column="vehicle_use" property="vehicleUse" /> |
| | |
| | | |
| | | <!-- 通用查询结果列 --> |
| | | <sql id="Base_Column_List"> |
| | | id, app_user_id, license_plate, vehicle_brand, vehicle_model, vehicle_use, endurance, create_time, del_flag |
| | | id, app_user_id, license_plate, vin, vehicle_brand, vehicle_model, vehicle_use, endurance, create_time, del_flag |
| | | </sql> |
| | | |
| | | |
| | |
| | | import com.ruoyi.chargingPile.api.query.GetPartnerList; |
| | | import com.ruoyi.chargingPile.api.query.SetPermissionConfiguration; |
| | | import com.ruoyi.chargingPile.api.domain.SiteMenu; |
| | | import com.ruoyi.chargingPile.dto.PartnerAppUserBindDTO; |
| | | import com.ruoyi.chargingPile.dto.PartnerAppUserVO; |
| | | import com.ruoyi.chargingPile.dto.ResetPassword; |
| | | import com.ruoyi.chargingPile.service.IPartnerService; |
| | | import com.ruoyi.chargingPile.service.TPartnerAppUserService; |
| | | import com.ruoyi.chargingPile.service.TPartnerSiteService; |
| | | import com.ruoyi.chargingPile.service.TSiteMenuService; |
| | | import com.ruoyi.common.core.domain.R; |
| | |
| | | |
| | | @Resource |
| | | private TPartnerSiteService partnerSiteService; |
| | | @Resource |
| | | private TPartnerAppUserService partnerAppUserService; |
| | | |
| | | |
| | | |
| | |
| | | public R<Partner> getPartnerR(@PathVariable("id") Integer id){ |
| | | Partner partner = partnerService.getPartner(id); |
| | | return R.ok(partner); |
| | | } |
| | | @ResponseBody |
| | | @PostMapping("/getPartnerByAppUserId/{appUserId}") |
| | | public R<Partner> getPartnerByAppUserId(@PathVariable("appUserId") Long appUserId) { |
| | | return R.ok(partnerAppUserService.getPartnerByAppUserId(appUserId)); |
| | | } |
| | | @ResponseBody |
| | | @GetMapping("/getAllPartner") |
| | |
| | | public AjaxResult resetPassword(@RequestBody ResetPassword resetPassword){ |
| | | return partnerService.resetPassword(resetPassword); |
| | | } |
| | | |
| | | @RequiresPermissions(value = {"/partners/update"}, logical = Logical.OR) |
| | | @GetMapping("/appUser/list") |
| | | @ApiOperation(value = "查询合作商可绑定的小程序用户", tags = {"管理后台-合作商管理"}) |
| | | public AjaxResult<PageInfo<PartnerAppUserVO>> getPartnerAppUserList(@RequestParam Integer partnerId, |
| | | @RequestParam(required = false) String phone, |
| | | @RequestParam(defaultValue = "1") Integer pageNum, |
| | | @RequestParam(defaultValue = "10") Integer pageSize) { |
| | | return AjaxResult.success(partnerAppUserService.searchUsers(partnerId, phone, pageNum, pageSize)); |
| | | } |
| | | |
| | | @RequiresPermissions(value = {"/partners/update"}, logical = Logical.OR) |
| | | @GetMapping("/appUser/bound/{partnerId}") |
| | | @ApiOperation(value = "查询合作商已绑定的小程序用户", tags = {"管理后台-合作商管理"}) |
| | | public AjaxResult<List<PartnerAppUserVO>> getBoundPartnerAppUsers(@PathVariable Integer partnerId) { |
| | | return AjaxResult.success(partnerAppUserService.getBoundUsers(partnerId)); |
| | | } |
| | | |
| | | @RequiresPermissions(value = {"/partners/update"}, logical = Logical.OR) |
| | | @PostMapping("/appUser/bind") |
| | | @ApiOperation(value = "保存合作商绑定的小程序用户", tags = {"管理后台-合作商管理"}) |
| | | @Log(title = "【合作商列表】绑定小程序用户", businessType = BusinessType.UPDATE) |
| | | public AjaxResult bindPartnerAppUsers(@RequestBody PartnerAppUserBindDTO bindDTO) { |
| | | partnerAppUserService.saveBindings(bindDTO); |
| | | return AjaxResult.success(); |
| | | } |
| | | |
| | | @Autowired |
| | | private TokenService tokenService; |
| | |
| | | siteInfoVO.setName(site.getName()); |
| | | siteInfoVO.setNumber(one.getNumber().toString()); |
| | | siteInfoVO.setSpaceChargeExplain(site.getSpaceChargeExplain()); |
| | | siteInfoVO.setSupportOrder(site.getSupportOrder()); |
| | | siteInfoVO.setReservationStartTime(site.getReservationStartTime()); |
| | | siteInfoVO.setReservationEndTime(site.getReservationEndTime()); |
| | | siteInfoVO.setVinFlag(one.getVinFlag()); |
| | | return R.ok(siteInfoVO); |
| | | } |
| | | /** |
| | |
| | | siteInfoVO.setName(site.getName()); |
| | | siteInfoVO.setNumber(one.getNumber().toString()); |
| | | siteInfoVO.setSpaceChargeExplain(site.getSpaceChargeExplain()); |
| | | siteInfoVO.setSupportOrder(site.getSupportOrder()); |
| | | siteInfoVO.setReservationStartTime(site.getReservationStartTime()); |
| | | siteInfoVO.setReservationEndTime(site.getReservationEndTime()); |
| | | siteInfoVO.setVinFlag(one.getVinFlag()); |
| | | return R.ok(siteInfoVO); |
| | | } |
| | | |
| | |
| | | List<Site> sites = siteService.list(new LambdaQueryWrapper<Site>().in(Site::getId, ids).eq(Site::getDelFlag, 0)); |
| | | return R.ok(sites); |
| | | } |
| | | |
| | | @GetMapping("/getSitesByIds") |
| | | @ApiOperation(value = "小程序批量获取站点基础信息", tags = {"小程序-收藏"}) |
| | | public R<List<Site>> getSitesByIds(@RequestParam String ids) { |
| | | List<Integer> siteIds = Arrays.stream(ids.split(",")) |
| | | .filter(StringUtils::isNotEmpty) |
| | | .map(Integer::valueOf) |
| | | .collect(Collectors.toList()); |
| | | if (siteIds.isEmpty()) { |
| | | return R.ok(Collections.emptyList()); |
| | | } |
| | | return R.ok(siteService.list(new LambdaQueryWrapper<Site>() |
| | | .in(Site::getId, siteIds) |
| | | .eq(Site::getDelFlag, 0))); |
| | | } |
| | | |
| | | @PostMapping("/favoriteSitePrices") |
| | | @ApiOperation(value = "批量获取收藏站点当前价格", tags = {"内部接口-收藏"}) |
| | | public R<List<FavoriteSitePriceVO>> favoriteSitePrices(@RequestBody List<Integer> ids) { |
| | | if (CollectionUtils.isEmpty(ids)) { |
| | | return R.ok(Collections.emptyList()); |
| | | } |
| | | List<Site> sites = siteService.list(new LambdaQueryWrapper<Site>() |
| | | .in(Site::getId, ids) |
| | | .eq(Site::getDelFlag, 0)); |
| | | List<FavoriteSitePriceVO> result = sites.stream().map(site -> { |
| | | FavoriteSitePriceVO vo = new FavoriteSitePriceVO(); |
| | | vo.setSiteId(site.getId()); |
| | | vo.setSiteName(site.getName()); |
| | | vo.setStatus(site.getStatus()); |
| | | vo.setCurrentPrice(getCurrentSitePrice(site)); |
| | | return vo; |
| | | }).collect(Collectors.toList()); |
| | | return R.ok(result); |
| | | } |
| | | |
| | | private BigDecimal getCurrentSitePrice(Site site) { |
| | | if (site.getAccountingStrategyId() == null) { |
| | | return null; |
| | | } |
| | | LocalTime now = LocalTime.now(); |
| | | List<TAccountingStrategyDetail> details = accountingStrategyDetailService.lambdaQuery() |
| | | .eq(TAccountingStrategyDetail::getAccountingStrategyId, site.getAccountingStrategyId()) |
| | | .list(); |
| | | for (TAccountingStrategyDetail detail : details) { |
| | | LocalTime start = parsePriceTime(detail.getStartTime(), false); |
| | | LocalTime end = parsePriceTime(detail.getEndTime(), true); |
| | | if (start != null && end != null && !now.isBefore(start) && !now.isAfter(end)) { |
| | | return detail.getElectrovalence().add(detail.getServiceCharge()) |
| | | .setScale(4, BigDecimal.ROUND_HALF_UP); |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private LocalTime parsePriceTime(String value, boolean endTime) { |
| | | if (StringUtils.isEmpty(value)) { |
| | | return null; |
| | | } |
| | | if (endTime && "00:00".equals(value)) { |
| | | return LocalTime.of(23, 59, 59); |
| | | } |
| | | return LocalTime.parse(value.length() == 5 ? value + ":00" : value); |
| | | } |
| | | |
| | | |
| | | /** |
| New file |
| | |
| | | package com.ruoyi.chargingPile.dto; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import javax.validation.constraints.NotNull; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | @Data |
| | | @ApiModel("合作商绑定小程序用户参数") |
| | | public class PartnerAppUserBindDTO { |
| | | |
| | | @NotNull(message = "合作商ID不能为空") |
| | | @ApiModelProperty(value = "合作商ID", required = true) |
| | | private Integer partnerId; |
| | | |
| | | @ApiModelProperty("小程序用户ID列表") |
| | | private List<Long> appUserIds = new ArrayList<>(); |
| | | } |
| New file |
| | |
| | | package com.ruoyi.chargingPile.dto; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import java.time.LocalDateTime; |
| | | |
| | | @Data |
| | | @ApiModel("合作商可绑定的小程序用户") |
| | | public class PartnerAppUserVO { |
| | | |
| | | @ApiModelProperty("小程序用户ID") |
| | | private Long appUserId; |
| | | |
| | | @ApiModelProperty("姓名") |
| | | private String name; |
| | | |
| | | @ApiModelProperty("手机号") |
| | | private String phone; |
| | | |
| | | @ApiModelProperty("备注") |
| | | private String remark; |
| | | |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") |
| | | @ApiModelProperty("创建时间") |
| | | private LocalDateTime createTime; |
| | | |
| | | @ApiModelProperty("当前绑定合作商ID") |
| | | private Integer partnerId; |
| | | |
| | | @ApiModelProperty("当前绑定合作商名称") |
| | | private String partnerName; |
| | | |
| | | @ApiModelProperty("是否已绑定当前合作商") |
| | | private Boolean selected; |
| | | |
| | | @ApiModelProperty("是否已绑定其他合作商") |
| | | private Boolean disabled; |
| | | } |
| New file |
| | |
| | | package com.ruoyi.chargingPile.mapper; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.ruoyi.chargingPile.api.model.TPartnerAppUser; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | |
| | | @Mapper |
| | | public interface TPartnerAppUserMapper extends BaseMapper<TPartnerAppUser> { |
| | | } |
| New file |
| | |
| | | package com.ruoyi.chargingPile.service; |
| | | |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import com.ruoyi.chargingPile.api.model.Partner; |
| | | import com.ruoyi.chargingPile.api.model.TPartnerAppUser; |
| | | import com.ruoyi.chargingPile.dto.PartnerAppUserBindDTO; |
| | | import com.ruoyi.chargingPile.dto.PartnerAppUserVO; |
| | | import com.ruoyi.common.core.web.page.PageInfo; |
| | | |
| | | import java.util.List; |
| | | |
| | | public interface TPartnerAppUserService extends IService<TPartnerAppUser> { |
| | | |
| | | PageInfo<PartnerAppUserVO> searchUsers(Integer partnerId, String phone, Integer pageNum, Integer pageSize); |
| | | |
| | | List<PartnerAppUserVO> getBoundUsers(Integer partnerId); |
| | | |
| | | void saveBindings(PartnerAppUserBindDTO bindDTO); |
| | | |
| | | Partner getPartnerByAppUserId(Long appUserId); |
| | | } |
| New file |
| | |
| | | package com.ruoyi.chargingPile.service.impl; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.ruoyi.account.api.feignClient.AppUserClient; |
| | | import com.ruoyi.account.api.model.TAppUser; |
| | | import com.ruoyi.chargingPile.api.model.Partner; |
| | | import com.ruoyi.chargingPile.api.model.TPartnerAppUser; |
| | | import com.ruoyi.chargingPile.dto.PartnerAppUserBindDTO; |
| | | import com.ruoyi.chargingPile.dto.PartnerAppUserVO; |
| | | import com.ruoyi.chargingPile.mapper.TPartnerAppUserMapper; |
| | | import com.ruoyi.chargingPile.service.IPartnerService; |
| | | import com.ruoyi.chargingPile.service.TPartnerAppUserService; |
| | | import com.ruoyi.common.core.domain.R; |
| | | import com.ruoyi.common.core.exception.ServiceException; |
| | | import com.ruoyi.common.core.web.page.PageInfo; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Set; |
| | | import java.util.function.Function; |
| | | import java.util.stream.Collectors; |
| | | |
| | | @Service |
| | | public class TPartnerAppUserServiceImpl extends ServiceImpl<TPartnerAppUserMapper, TPartnerAppUser> |
| | | implements TPartnerAppUserService { |
| | | |
| | | @Resource |
| | | private AppUserClient appUserClient; |
| | | |
| | | @Resource |
| | | private IPartnerService partnerService; |
| | | |
| | | @Override |
| | | public PageInfo<PartnerAppUserVO> searchUsers(Integer partnerId, String phone, Integer pageNum, Integer pageSize) { |
| | | assertPartnerExists(partnerId); |
| | | int current = pageNum == null || pageNum < 1 ? 1 : pageNum; |
| | | int size = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 100); |
| | | R<Page<TAppUser>> result = appUserClient.searchPartnerBindUsers(phone, current, size); |
| | | Page<TAppUser> userPage = result == null ? null : result.getData(); |
| | | if (userPage == null) { |
| | | throw new ServiceException("小程序用户服务暂不可用"); |
| | | } |
| | | |
| | | List<TAppUser> users = userPage.getRecords(); |
| | | Map<Long, TPartnerAppUser> relationMap = getRelationMap(users); |
| | | Map<Integer, Partner> partnerMap = getPartnerMap(relationMap.values()); |
| | | List<PartnerAppUserVO> records = users.stream() |
| | | .map(user -> toVO(user, relationMap.get(user.getId()), partnerMap, partnerId)) |
| | | .collect(Collectors.toList()); |
| | | |
| | | PageInfo<PartnerAppUserVO> page = new PageInfo<>(userPage.getCurrent(), userPage.getSize()); |
| | | page.setTotal(userPage.getTotal()); |
| | | page.setRecords(records); |
| | | return page; |
| | | } |
| | | |
| | | @Override |
| | | public List<PartnerAppUserVO> getBoundUsers(Integer partnerId) { |
| | | assertPartnerExists(partnerId); |
| | | List<TPartnerAppUser> relations = list(new LambdaQueryWrapper<TPartnerAppUser>() |
| | | .eq(TPartnerAppUser::getPartnerId, partnerId) |
| | | .orderByDesc(TPartnerAppUser::getCreateTime)); |
| | | if (relations.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | List<Long> userIds = relations.stream() |
| | | .map(TPartnerAppUser::getAppUserId) |
| | | .collect(Collectors.toList()); |
| | | R<List<TAppUser>> userResult = appUserClient.getUserByIds(userIds); |
| | | List<TAppUser> users = userResult == null ? null : userResult.getData(); |
| | | if (users == null) { |
| | | throw new ServiceException("小程序用户服务暂不可用"); |
| | | } |
| | | Map<Long, TAppUser> userMap = users.stream() |
| | | .collect(Collectors.toMap(TAppUser::getId, Function.identity(), (left, right) -> left)); |
| | | Partner partner = partnerService.getById(partnerId); |
| | | Map<Integer, Partner> partnerMap = Collections.singletonMap(partnerId, partner); |
| | | |
| | | return relations.stream() |
| | | .map(relation -> { |
| | | TAppUser user = userMap.get(relation.getAppUserId()); |
| | | return user == null ? null : toVO(user, relation, partnerMap, partnerId); |
| | | }) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void saveBindings(PartnerAppUserBindDTO bindDTO) { |
| | | Integer partnerId = bindDTO.getPartnerId(); |
| | | assertPartnerExists(partnerId); |
| | | Set<Long> selectedIds = bindDTO.getAppUserIds() == null |
| | | ? Collections.emptySet() |
| | | : bindDTO.getAppUserIds().stream() |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toCollection(LinkedHashSet::new)); |
| | | |
| | | if (!selectedIds.isEmpty()) { |
| | | long conflictCount = count(new LambdaQueryWrapper<TPartnerAppUser>() |
| | | .in(TPartnerAppUser::getAppUserId, selectedIds) |
| | | .ne(TPartnerAppUser::getPartnerId, partnerId)); |
| | | if (conflictCount > 0) { |
| | | throw new ServiceException("所选手机号已绑定其他合作商,请刷新列表后重试"); |
| | | } |
| | | } |
| | | |
| | | remove(new LambdaQueryWrapper<TPartnerAppUser>().eq(TPartnerAppUser::getPartnerId, partnerId)); |
| | | if (selectedIds.isEmpty()) { |
| | | return; |
| | | } |
| | | |
| | | List<TPartnerAppUser> relations = new ArrayList<>(selectedIds.size()); |
| | | for (Long appUserId : selectedIds) { |
| | | TPartnerAppUser relation = new TPartnerAppUser(); |
| | | relation.setPartnerId(partnerId); |
| | | relation.setAppUserId(appUserId); |
| | | relations.add(relation); |
| | | } |
| | | saveBatch(relations); |
| | | } |
| | | |
| | | @Override |
| | | public Partner getPartnerByAppUserId(Long appUserId) { |
| | | if (appUserId == null) { |
| | | return null; |
| | | } |
| | | TPartnerAppUser relation = getOne(new LambdaQueryWrapper<TPartnerAppUser>() |
| | | .eq(TPartnerAppUser::getAppUserId, appUserId) |
| | | .last("limit 1")); |
| | | return relation == null ? null : partnerService.getById(relation.getPartnerId()); |
| | | } |
| | | |
| | | private Map<Long, TPartnerAppUser> getRelationMap(List<TAppUser> users) { |
| | | if (users == null || users.isEmpty()) { |
| | | return Collections.emptyMap(); |
| | | } |
| | | List<Long> userIds = users.stream().map(TAppUser::getId).collect(Collectors.toList()); |
| | | return list(new LambdaQueryWrapper<TPartnerAppUser>() |
| | | .in(TPartnerAppUser::getAppUserId, userIds)).stream() |
| | | .collect(Collectors.toMap(TPartnerAppUser::getAppUserId, Function.identity(), (left, right) -> left)); |
| | | } |
| | | |
| | | private Map<Integer, Partner> getPartnerMap(java.util.Collection<TPartnerAppUser> relations) { |
| | | Set<Integer> partnerIds = relations.stream() |
| | | .map(TPartnerAppUser::getPartnerId) |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toSet()); |
| | | if (partnerIds.isEmpty()) { |
| | | return Collections.emptyMap(); |
| | | } |
| | | return partnerService.listByIds(partnerIds).stream() |
| | | .collect(Collectors.toMap(Partner::getId, Function.identity(), (left, right) -> left)); |
| | | } |
| | | |
| | | private PartnerAppUserVO toVO(TAppUser user, TPartnerAppUser relation, |
| | | Map<Integer, Partner> partnerMap, Integer currentPartnerId) { |
| | | PartnerAppUserVO vo = new PartnerAppUserVO(); |
| | | vo.setAppUserId(user.getId()); |
| | | vo.setName(user.getName()); |
| | | vo.setPhone(user.getPhone()); |
| | | vo.setRemark(user.getRemark()); |
| | | vo.setCreateTime(user.getCreateTime()); |
| | | if (relation != null) { |
| | | vo.setPartnerId(relation.getPartnerId()); |
| | | Partner partner = partnerMap.get(relation.getPartnerId()); |
| | | vo.setPartnerName(partner == null ? null : partner.getName()); |
| | | } |
| | | boolean selected = relation != null && Objects.equals(relation.getPartnerId(), currentPartnerId); |
| | | vo.setSelected(selected); |
| | | vo.setDisabled(relation != null && !selected); |
| | | return vo; |
| | | } |
| | | |
| | | private void assertPartnerExists(Integer partnerId) { |
| | | if (partnerId == null || partnerService.getById(partnerId) == null) { |
| | | throw new ServiceException("合作商不存在"); |
| | | } |
| | | } |
| | | } |
| | |
| | | select |
| | | ts.id, ts.partner_id, ts.code, ts.`name`, ts.site_type, ts.business_category, ts.status, ts.construction_site, ts.img_url, |
| | | ts.lon, ts.lat, ts.address, ts.country_code, ts.phone,ts.guide, ts.service_description, ts.vehicle_description, |
| | | ts.parking_space, ts.rate_description, ts.space_charge_explain, ts.accounting_strategy_id,ts.del_flag,tcg.fastCount, |
| | | ts.parking_space, ts.rate_description, ts.space_charge_explain, ts.accounting_strategy_id, ts.park_type as parkType, ts.park_fee as parkFee, ts.park_fee_free as parkFeeFree, ts.supportOrder as supportOrder, ts.equipment_owner_name as equipmentOwnerName, ts.rated_power as ratedPower, ts.del_flag,tcg.fastCount, |
| | | tcg.slowCount,tcg.superCount,tasd.electrovalence,tasd.vipElectrovalence,tasd.serviceCharge,tasd.electrovalenceOriginal,tcg.superFreeCount,tcg.fastFreeCount,tcg.slowFreeCount, |
| | | ROUND( |
| | | 6378.138 * 2 * ASIN( |
| | |
| | | ORDER BY tasd.electrovalence ASC |
| | | </when> |
| | | <when test="query.sortType == 3"> |
| | | ORDER BY (tcg.fastCount + tcg.slowCount + tcg.superCount) DESC |
| | | ORDER BY (tcg.fastFreeCount + tcg.slowFreeCount + tcg.superFreeCount) DESC |
| | | </when> |
| | | </choose> |
| | | </if> |
| | |
| | | select |
| | | ts.id, ts.partner_id, ts.code, ts.`name`, ts.site_type, ts.business_category, ts.status, ts.construction_site, ts.img_url, |
| | | ts.lon, ts.lat, ts.address, ts.country_code, ts.phone,ts.guide, ts.service_description, ts.vehicle_description, |
| | | ts.parking_space, ts.rate_description, ts.space_charge_explain, ts.accounting_strategy_id,ts.del_flag,tcg.fastCount, |
| | | ts.parking_space, ts.rate_description, ts.space_charge_explain, ts.accounting_strategy_id, ts.park_type as parkType, ts.park_fee as parkFee, ts.park_fee_free as parkFeeFree, ts.supportOrder as supportOrder, ts.equipment_owner_name as equipmentOwnerName, ts.rated_power as ratedPower, ts.del_flag,tcg.fastCount, |
| | | tcg.slowCount,tcg.superCount,tasd.electrovalence,tasd.vipElectrovalence,tcg.superFreeCount,tcg.fastFreeCount,tcg.slowFreeCount, |
| | | ROUND( |
| | | 6378.138 * 2 * ASIN( |
| | |
| | | private Integer secure_connection; // 车枪连接(0=未连接,1=连接) |
| | | private Integer nsulation_detection; // 绝缘检测(0=安全,1=不安全) |
| | | private Integer electronic_lock_lock; // 电磁锁状态(0=未锁,1=已锁) |
| | | private String vin; // 当前车辆VIN码 |
| | | |
| | | } |
| | | |
| | |
| | | package com.ruoyi.order.dto; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | |
| | | import io.swagger.annotations.ApiModel; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import lombok.Data; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.LocalDateTime; |
| | | |
| | | /** |
| | | * @author zhibing.pu |
| | |
| | | private Integer payMethod; |
| | | @ApiModelProperty("回跳地址") |
| | | private String returnUrl; |
| | | @ApiModelProperty("预约充电时间") |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | | private LocalDateTime reservationTime; |
| | | |
| | | } |
| | |
| | | public class MyChargingOrderList { |
| | | @ApiModelProperty("订单id") |
| | | private String id; |
| | | @ApiModelProperty("站点id") |
| | | private Integer siteId; |
| | | @ApiModelProperty("订单状态(0=未知,1=等待中,2=启动中,3=充电中,4=停止中,5=已结束)") |
| | | private Integer status; |
| | | @ApiModelProperty("标题") |
| | |
| | | private Integer startupSuccess; |
| | | @ApiModelProperty("失败原因(0:无,1:设备编号不匹配,2:枪已在充电,3:设备故障,4:设备离线,5:未插枪桩在收到启充命令后,检测到未插枪则发送 0x33 报文回复充电失败。若在 60 秒(以收到 0x34 时间开始计算)内检测到枪重新连接,则补送 0x33 成功报文;超时或者离线等其他异常,桩不启充、不补发 0x33 报文)") |
| | | private Integer failureCause; |
| | | @ApiModelProperty("设备检测到的车辆VIN") |
| | | private String actualVin; |
| | | @ApiModelProperty("启动命令是否已下发") |
| | | private Boolean startCommandSent; |
| | | @ApiModelProperty("添加时间") |
| | | private LocalDateTime addTime; |
| | | } |
| | |
| | | for (TChargingOrder tChargingOrder : list) { |
| | | MyChargingOrderList myChargingOrderList = new MyChargingOrderList(); |
| | | myChargingOrderList.setId(tChargingOrder.getId().toString()); |
| | | myChargingOrderList.setSiteId(tChargingOrder.getSiteId()); |
| | | myChargingOrderList.setStatus(tChargingOrder.getStatus()); |
| | | Site site = siteClient.getSiteByIds(Arrays.asList(tChargingOrder.getSiteId())).getData().get(0); |
| | | myChargingOrderList.setTitle(site.getName()); |
| | |
| | | for (TChargingOrder tChargingOrder : orderList) { |
| | | MyChargingOrderList myChargingOrderList = new MyChargingOrderList(); |
| | | myChargingOrderList.setId(tChargingOrder.getId().toString()); |
| | | myChargingOrderList.setSiteId(tChargingOrder.getSiteId()); |
| | | myChargingOrderList.setStatus(tChargingOrder.getStatus()); |
| | | Site site = siteClient.getSiteByIds(Arrays.asList(tChargingOrder.getSiteId())).getData().get(0); |
| | | myChargingOrderList.setTitle(site.getName()); |
| | |
| | | BigDecimal rechargeAmount = money(addChargingOrder.getPaymentAmount()); |
| | | TChargingGun tChargingGun = chargingGunClient.getChargingGunById(addChargingOrder.getId()).getData(); |
| | | TChargingPile chargingPile = chargingPileClient.getChargingPileById(tChargingGun.getChargingPileId()).getData(); |
| | | TAppUserCar selectedCar = null; |
| | | if (addChargingOrder.getAppUserCarId() != null) { |
| | | selectedCar = appUserCarClient.getCarById(addChargingOrder.getAppUserCarId().toString()).getData(); |
| | | } |
| | | String selectedVin = selectedCar == null ? null : normalizeVin(selectedCar.getVin()); |
| | | if (Integer.valueOf(1).equals(chargingPile.getVinFlag()) |
| | | && (!StringUtils.hasLength(selectedVin) || !selectedVin.matches("[A-HJ-NPR-Z0-9]{17}"))) { |
| | | return AjaxResult.error("该充电桩需要17位有效车辆VIN,请先完善所选车辆VIN"); |
| | | } |
| | | AppUserWhitelistMatchVO whitelist = appUserWhitelistClient.matchWhitelist(userId, tChargingGun.getSiteId()).getData(); |
| | | boolean whitelistMatched = whitelist != null && Boolean.TRUE.equals(whitelist.getMatched()); |
| | | if (whitelistMatched) { |
| | |
| | | chargingOrder.setOrderClassification(1); |
| | | chargingOrder.setAppUserId(userId); |
| | | chargingOrder.setAppUserCarId(addChargingOrder.getAppUserCarId()); |
| | | if (null != addChargingOrder.getAppUserCarId()) { |
| | | TAppUserCar userCar = appUserCarClient.getCarById(addChargingOrder.getAppUserCarId().toString()).getData(); |
| | | if (null != userCar) { |
| | | chargingOrder.setPlateNum(userCar.getLicensePlate()); |
| | | } |
| | | if (selectedCar != null) { |
| | | chargingOrder.setPlateNum(selectedCar.getLicensePlate()); |
| | | chargingOrder.setExpectedVin(selectedVin); |
| | | } |
| | | chargingOrder.setReservationTime(addChargingOrder.getReservationTime()); |
| | | chargingOrder.setSiteId(tChargingGun.getSiteId()); |
| | | chargingOrder.setChargingPileId(tChargingGun.getChargingPileId()); |
| | | chargingOrder.setChargingGunId(addChargingOrder.getId()); |
| | |
| | | chargingOrder.setTitle("【充电桩充电】" + chargingPile.getNumber() + "号桩/" + tChargingGun.getCode() + "号枪"); |
| | | |
| | | Site site = siteClient.getSiteByIds(Arrays.asList(tChargingGun.getSiteId())).getData().get(0); |
| | | AjaxResult reservationValidation = validateReservation(site, addChargingOrder.getReservationTime()); |
| | | if (reservationValidation != null) { |
| | | return reservationValidation; |
| | | } |
| | | Integer accountingStrategyId = tChargingGun.getAccountingStrategyId(); |
| | | if (null == accountingStrategyId) { |
| | | //查询站点上面的计费策略 |
| | |
| | | preChargeCheck.setInsulationTesting(false); |
| | | preChargeCheck.setSecureConnectionDetection(false); |
| | | preChargeCheck.setStartupSuccess(1); |
| | | preChargeCheck.setAddTime(LocalDateTime.now()); |
| | | preChargeCheck.setStartCommandSent(false); |
| | | preChargeCheck.setAddTime(chargingOrder.getReservationTime() != null ? chargingOrder.getReservationTime() : LocalDateTime.now()); |
| | | String key = "AQJC_" + chargingOrder.getChargingGunId(); |
| | | redisService.setCacheObject(key, preChargeCheck, 24L, TimeUnit.HOURS); |
| | | |
| | |
| | | this.updateById(chargingOrder); |
| | | |
| | | |
| | | TChargingPile chargingPile = chargingPileClient.getChargingPileById(tChargingGun.getChargingPileId()).getData(); |
| | | //调用远程启动充电消息 |
| | | PlatformStartCharging platformStartCharging = new PlatformStartCharging(); |
| | | platformStartCharging.setTransaction_serial_number(chargingOrder.getCode()); |
| | | platformStartCharging.setCharging_pile_code(chargingPile.getCode()); |
| | | platformStartCharging.setCharging_gun_code(tChargingGun.getCode()); |
| | | //使用订单id作为逻辑卡号 |
| | | platformStartCharging.setCard_number(chargingOrder.getId().toString()); |
| | | platformStartCharging.setAccount_balance(account_balance); |
| | | |
| | | log.info(chargingOrder.getCode() + ":-------------------远程调起开始充电请求-------------------" + platformStartCharging.toString()); |
| | | sendMessageClient.platformStartCharging(platformStartCharging); |
| | | |
| | | ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingDeque<>()); |
| | | threadPoolExecutor.execute(()->{ |
| | | //推送成都监管 |
| | | NotificationConnectorChargeStatusVo vo = new NotificationConnectorChargeStatusVo(); |
| | | vo.setOrderCode(chargingOrder.getCode()); |
| | | vo.setChargingGunId(chargingOrder.getChargingGunId()); |
| | | chengDuJianGuanClient.notificationConnectorChargeStatus(vo); |
| | | }); |
| | | |
| | | |
| | | //异步线程检测远程启动的应答结果。如果失败,则需要全额退款 |
| | | Long id = chargingOrder.getId(); |
| | | //执行10分钟的定时任务检测 |
| | | ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); |
| | | scheduler.scheduleAtFixedRate(()->{ |
| | | if(timingDetection(id)){ |
| | | scheduler.shutdown(); |
| | | } |
| | | }, 10, 1, TimeUnit.SECONDS); |
| | | scheduleReservationStart(chargingOrder); |
| | | return AjaxResult.success(); |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | private AjaxResult validateReservation(Site site, LocalDateTime reservationTime) { |
| | | if (reservationTime == null) { |
| | | return null; |
| | | } |
| | | if (!Integer.valueOf(1).equals(site.getSupportOrder())) { |
| | | return AjaxResult.error("该电站暂不支持预约充电"); |
| | | } |
| | | if (reservationTime.isBefore(LocalDateTime.now().minusMinutes(1))) { |
| | | return AjaxResult.error("预约时间不能早于当前时间"); |
| | | } |
| | | if (!StringUtils.hasLength(site.getReservationStartTime()) |
| | | || !StringUtils.hasLength(site.getReservationEndTime())) { |
| | | return AjaxResult.error("电站尚未配置预约时段"); |
| | | } |
| | | LocalTime start = LocalTime.parse(site.getReservationStartTime(), DateTimeFormatter.ofPattern("HH:mm")); |
| | | LocalTime end = LocalTime.parse(site.getReservationEndTime(), DateTimeFormatter.ofPattern("HH:mm")); |
| | | if (!isTimeInWindow(LocalTime.now(), start, end) |
| | | || !isTimeInWindow(reservationTime.toLocalTime(), start, end)) { |
| | | return AjaxResult.error("当前时间或预约时间不在允许预约时段内"); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private boolean isTimeInWindow(LocalTime value, LocalTime start, LocalTime end) { |
| | | if (start.equals(end)) { |
| | | return true; |
| | | } |
| | | if (start.isBefore(end)) { |
| | | return !value.isBefore(start) && !value.isAfter(end); |
| | | } |
| | | return !value.isBefore(start) || !value.isAfter(end); |
| | | } |
| | | |
| | | private boolean isSafetyCheckPassed(PreChargeCheck check) { |
| | | return check != null |
| | | && Boolean.TRUE.equals(check.getElectronicLockLock()) |
| | | && Boolean.TRUE.equals(check.getInsulationTesting()) |
| | | && Boolean.TRUE.equals(check.getSecureConnectionDetection()); |
| | | } |
| | | |
| | | private String normalizeVin(String vin) { |
| | | return StringUtils.hasLength(vin) ? vin.replaceAll("\\s+", "").toUpperCase(Locale.ROOT) : null; |
| | | } |
| | | |
| | | private boolean vinMatches(String expectedVin, String actualVin) { |
| | | if (!StringUtils.hasLength(expectedVin) || !StringUtils.hasLength(actualVin)) { |
| | | return false; |
| | | } |
| | | return expectedVin.equals(actualVin) |
| | | || expectedVin.equals(new StringBuilder(actualVin).reverse().toString()); |
| | | } |
| | | |
| | | private void scheduleReservationStart(TChargingOrder chargingOrder) { |
| | | if (chargingOrder.getReservationTime() == null || !chargingOrder.getReservationTime().isAfter(LocalDateTime.now())) { |
| | | return; |
| | | } |
| | | long delay = Math.max(0L, Duration.between(LocalDateTime.now(), chargingOrder.getReservationTime()).toMillis()); |
| | | ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); |
| | | scheduler.schedule(() -> { |
| | | try { |
| | | TChargingOrder latest = this.getById(chargingOrder.getId()); |
| | | if (latest == null || latest.getStatus() != 2) { |
| | | return; |
| | | } |
| | | PreChargeCheck check = redisService.getCacheObject("AQJC_" + latest.getChargingGunId()); |
| | | if (isSafetyCheckPassed(check)) { |
| | | startChargingAfterSafety(latest, check); |
| | | } |
| | | } finally { |
| | | scheduler.shutdown(); |
| | | } |
| | | }, delay, TimeUnit.MILLISECONDS); |
| | | } |
| | | |
| | | private void startChargingAfterSafety(TChargingOrder chargingOrder, PreChargeCheck check) { |
| | | RLock lock = redissonClient.getLock("START_AFTER_SAFETY:" + chargingOrder.getId()); |
| | | try { |
| | | if (!lock.tryLock(1, 10, TimeUnit.SECONDS)) { |
| | | return; |
| | | } |
| | | TChargingOrder latest = this.getById(chargingOrder.getId()); |
| | | if (latest == null || latest.getStatus() != 2 || Boolean.TRUE.equals(check.getStartCommandSent())) { |
| | | return; |
| | | } |
| | | if (latest.getReservationTime() != null && latest.getReservationTime().isAfter(LocalDateTime.now())) { |
| | | return; |
| | | } |
| | | TChargingGun gun = chargingGunClient.getChargingGunById(latest.getChargingGunId()).getData(); |
| | | TChargingPile pile = chargingPileClient.getChargingPileById(gun.getChargingPileId()).getData(); |
| | | String actualVin = normalizeVin(check.getActualVin()); |
| | | String expectedVin = normalizeVin(latest.getExpectedVin()); |
| | | if (Integer.valueOf(1).equals(pile.getVinFlag()) |
| | | && !vinMatches(expectedVin, actualVin)) { |
| | | check.setStartupSuccess(3); |
| | | check.setFailureCause(9); |
| | | latest.setActualVin(actualVin); |
| | | this.updateById(latest); |
| | | redisService.setCacheObject("AQJC_" + latest.getChargingGunId(), check, 24L, TimeUnit.HOURS); |
| | | refund(latest.getCode()); |
| | | return; |
| | | } |
| | | PlatformStartCharging command = new PlatformStartCharging(); |
| | | command.setTransaction_serial_number(latest.getCode()); |
| | | command.setCharging_pile_code(pile.getCode()); |
| | | command.setCharging_gun_code(gun.getCode()); |
| | | command.setCard_number(latest.getId().toString()); |
| | | command.setAccount_balance(latest.getChargeAmount()); |
| | | check.setStartCommandSent(true); |
| | | latest.setActualVin(actualVin); |
| | | this.updateById(latest); |
| | | redisService.setCacheObject("AQJC_" + latest.getChargingGunId(), check, 24L, TimeUnit.HOURS); |
| | | log.info(latest.getCode() + ":安全检测及VIN校验通过,下发启动充电命令:" + command); |
| | | sendMessageClient.platformStartCharging(command); |
| | | ScheduledExecutorService resultScheduler = Executors.newScheduledThreadPool(1); |
| | | resultScheduler.scheduleAtFixedRate(() -> { |
| | | if (timingDetection(latest.getId())) { |
| | | resultScheduler.shutdown(); |
| | | } |
| | | }, 10, 1, TimeUnit.SECONDS); |
| | | } catch (InterruptedException e) { |
| | | Thread.currentThread().interrupt(); |
| | | throw new ServiceException("启动充电锁等待被中断"); |
| | | } finally { |
| | | if (lock.isHeldByCurrentThread()) { |
| | | lock.unlock(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 定时检测mongodb数据库数据 |
| | | * |
| | |
| | | TChargingGun chargingGun = chargingGunClient.getChargingGunByCode(code).getData(); |
| | | PreChargeCheck preChargeCheck1 = redisService.getCacheObject("AQJC_" + chargingGun.getId()); |
| | | if (null != preChargeCheck1) { |
| | | preChargeCheck1.setElectronicLockLock(true); |
| | | preChargeCheck1.setInsulationTesting(true); |
| | | preChargeCheck1.setSecureConnectionDetection(securityDetection.getSecure_connection() == 1); |
| | | preChargeCheck1.setElectronicLockLock(Integer.valueOf(1).equals(securityDetection.getElectronic_lock_lock())); |
| | | preChargeCheck1.setInsulationTesting(Integer.valueOf(0).equals(securityDetection.getNsulation_detection())); |
| | | preChargeCheck1.setSecureConnectionDetection(Integer.valueOf(1).equals(securityDetection.getSecure_connection())); |
| | | preChargeCheck1.setActualVin(normalizeVin(securityDetection.getVin())); |
| | | redisService.setCacheObject("AQJC_" + chargingGun.getId(), preChargeCheck1, 24L, TimeUnit.HOURS); |
| | | if (isSafetyCheckPassed(preChargeCheck1)) { |
| | | TChargingOrder pendingOrder = this.getOne(new LambdaQueryWrapper<TChargingOrder>() |
| | | .eq(TChargingOrder::getChargingGunId, chargingGun.getId()) |
| | | .eq(TChargingOrder::getRechargePaymentStatus, 2) |
| | | .eq(TChargingOrder::getStatus, 2) |
| | | .eq(TChargingOrder::getDelFlag, 0) |
| | | .orderByDesc(TChargingOrder::getCreateTime) |
| | | .last("limit 1")); |
| | | if (pendingOrder != null) { |
| | | startChargingAfterSafety(pendingOrder, preChargeCheck1); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | preChargeCheck.setInsulationTesting(false); |
| | | preChargeCheck.setSecureConnectionDetection(false); |
| | | preChargeCheck.setStartupSuccess(1); |
| | | preChargeCheck.setAddTime(LocalDateTime.now()); |
| | | preChargeCheck.setStartCommandSent(false); |
| | | preChargeCheck.setAddTime(chargingOrder.getReservationTime() != null ? chargingOrder.getReservationTime() : LocalDateTime.now()); |
| | | String key = "AQJC_" + chargingOrder.getChargingGunId(); |
| | | redisService.setCacheObject(key, preChargeCheck, 24L, TimeUnit.HOURS); |
| | | |
| New file |
| | |
| | | -- 扫码充电 V2.0:预约时段与车辆 VIN 校验 |
| | | ALTER TABLE charging_pile_service.t_site |
| | | ADD COLUMN reservation_start_time varchar(5) NULL COMMENT '允许预约开始时间(HH:mm)' AFTER supportOrder, |
| | | ADD COLUMN reservation_end_time varchar(5) NULL COMMENT '允许预约结束时间(HH:mm)' AFTER reservation_start_time; |
| | | |
| | | ALTER TABLE charging_pile_account.t_app_user_car |
| | | ADD COLUMN vin varchar(64) NULL COMMENT '车辆VIN码' AFTER license_plate; |
| | | |
| | | ALTER TABLE charging_pile_order.t_charging_order |
| | | ADD COLUMN reservation_time datetime NULL COMMENT '预约充电时间' AFTER plate_num, |
| | | ADD COLUMN expected_vin varchar(64) NULL COMMENT '支付车辆VIN快照' AFTER reservation_time, |
| | | ADD COLUMN actual_vin varchar(64) NULL COMMENT '启动时设备识别VIN' AFTER expected_vin; |
| New file |
| | |
| | | USE `charging_pile_service`; |
| | | |
| | | CREATE TABLE IF NOT EXISTS `t_partner_app_user` ( |
| | | `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', |
| | | `partner_id` int NOT NULL COMMENT '合作商ID', |
| | | `app_user_id` bigint NOT NULL COMMENT '小程序用户ID', |
| | | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `uk_app_user_id` (`app_user_id`), |
| | | KEY `idx_partner_id` (`partner_id`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合作商绑定小程序用户'; |
| New file |
| | |
| | | CREATE TABLE IF NOT EXISTS `charging_pile_account`.`t_app_user_site_favorite` ( |
| | | `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', |
| | | `app_user_id` bigint NOT NULL COMMENT '小程序用户ID', |
| | | `site_id` int NOT NULL COMMENT '站点ID', |
| | | `favorite_price` decimal(10,4) NOT NULL DEFAULT 0.0000 COMMENT '收藏时电价', |
| | | `notify_enabled` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否允许降价提醒', |
| | | `last_notified_price` decimal(10,4) DEFAULT NULL COMMENT '最近一次已提醒价格', |
| | | `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '收藏时间', |
| | | `del_flag` tinyint(1) NOT NULL DEFAULT 0 COMMENT '删除标记', |
| | | PRIMARY KEY (`id`), |
| | | UNIQUE KEY `uk_user_site` (`app_user_id`,`site_id`), |
| | | KEY `idx_notify` (`notify_enabled`,`del_flag`) |
| | | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小程序用户收藏站点'; |
| | | |
| | | -- Nacos 的 ruoyi-account 配置需要补充真实微信订阅消息模板 ID: |
| | | -- wx.applet.price-drop-template-id: 你的模板ID |