diff --git a/hiver-admin/test-output/test-report.html b/hiver-admin/test-output/test-report.html index 7342b8b3..536b418c 100644 --- a/hiver-admin/test-output/test-report.html +++ b/hiver-admin/test-output/test-report.html @@ -35,7 +35,7 @@ Hiver
  • - 22, 2026 09:13:14 + 26, 2026 17:19:19
  • @@ -84,7 +84,7 @@

    passTest

    -

    09:13:14 / 0.021 secs

    +

    17:19:19 / 0.023 secs

    @@ -92,9 +92,9 @@
    #test-id=1
    passTest
    -07.22.2026 09:13:14 -07.22.2026 09:13:14 -0.021 secs +07.26.2026 17:19:19 +07.26.2026 17:19:19 +0.023 secs
    @@ -104,7 +104,7 @@ Pass - 9:13:14 + 17:19:19 Test passed @@ -128,13 +128,13 @@

    Started

    -

    22, 2026 09:13:14

    +

    26, 2026 17:19:19

    Ended

    -

    22, 2026 09:13:14

    +

    26, 2026 17:19:19

    diff --git a/hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/AuthController.java b/hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/AuthController.java index a7bcba19..f1bcf7eb 100644 --- a/hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/AuthController.java +++ b/hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/AuthController.java @@ -18,25 +18,28 @@ import cc.hiver.core.config.properties.HiverTokenProperties; import cc.hiver.core.entity.Role; import cc.hiver.core.entity.User; import cc.hiver.core.entity.UserRole; +import cc.hiver.core.entity.Worker; import cc.hiver.core.service.RoleService; import cc.hiver.core.service.UserRoleService; import cc.hiver.core.service.UserService; -import cc.hiver.mall.debt.service.DebtService; +import cc.hiver.core.service.WorkerService; import cc.hiver.mall.entity.Shop; import cc.hiver.mall.entity.ShopTakeaway; import cc.hiver.mall.entity.ShopUser; +import cc.hiver.mall.entity.WorkerRelaPrice; import cc.hiver.mall.invitelog.constant.InviteLogConstant; import cc.hiver.mall.invitelog.entity.InviteLog; import cc.hiver.mall.invitelog.service.InviteLogService; +import cc.hiver.mall.pojo.vo.WorkerRedisVo; import cc.hiver.mall.service.ShopService; import cc.hiver.mall.service.ShopTakeawayService; import cc.hiver.mall.service.ShopUserService; -import cc.hiver.mall.service.SupplierService; -import cc.hiver.mall.service.mybatis.CustomerService; -import cc.hiver.mall.service.mybatis.DealingsRecordService; import cc.hiver.mall.service.mybatis.ProductCategoryService; +import cc.hiver.mall.service.mybatis.WorkerRelaPriceService; +import cc.hiver.mall.utils.WorkerRedisCacheUtil; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; @@ -52,10 +55,7 @@ import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.TimeUnit; /** @@ -109,24 +109,21 @@ public class AuthController { private ShopUserService shopUserService; @Autowired - private ProductCategoryService productCategoryService; - @PersistenceContext - private EntityManager entityManager; - - @Autowired - private InviteLogService inviteLogService; + private WorkerService workerService; @Autowired - private SupplierService supplierService; + private WorkerRelaPriceService workerRelaPriceService; @Autowired - private CustomerService customerService; + private ProductCategoryService productCategoryService; + @PersistenceContext + private EntityManager entityManager; @Autowired - private DebtService debtService; + private InviteLogService inviteLogService; @Autowired - private DealingsRecordService dealingsRecordService; + WorkerRedisCacheUtil workerRedisCacheUtil; @RequestMapping(value = "/login", method = RequestMethod.POST) @SystemLog(description = "账号登录", type = LogType.LOGIN) @@ -177,31 +174,73 @@ public class AuthController { // 返回当前登录人的信息 result.put("user", user); - // 店铺登录的话,根据登录人查询所有关联的店铺(店铺启用,并且在有效期内) - if (UserConstant.USER_TYPE_NORMAL.equals(type)) { - // 获取 - final List shopUsers = shopUserService.selectByUserId(user.getId()); - if (shopUsers != null && !shopUsers.isEmpty()) { - // 获取店主手机号 - for (ShopUser shopUser : shopUsers) { - final Shop shop = shopService.findById(shopUser.getShop().getId()); - if (shop != null && shop.getShopOwnerId() != null && StringUtils.isNotEmpty(shop.getShopOwnerId())) { - shop.setClientId(clientId); - shopService.update(shop); - shopService.refreshShopCache(shop.getId(), shop.getRegionId()); - final User shopOwner = userService.findById(shop.getShopOwnerId()); - if(shopOwner != null){ - shopUser.setShopOwnerName(shopOwner.getNickname()); - shopUser.setShopOwnerPhone(shopOwner.getMobile()); + List shop = shopService.getShopByUserid(user.getId()); + if(!shop.isEmpty()){ + List shopIds = new ArrayList<>(); + for (Shop item : shop) { + shopIds.add(item.getId()); + if (StringUtils.isNotEmpty(clientId)) { + item.setClientId(clientId); + shopService.update(item); + shopService.refreshShopCache(item.getId(), item.getRegionId()); + } + } + if(!shopIds.isEmpty()){ + List shopTakeaway = shopTakeawayService.selectListByshopId(shopIds); + if(shopTakeaway != null){ + shop.forEach(e -> { + for(ShopTakeaway shopTakeawa : shopTakeaway) { + if (e.getId().equals(shopTakeawa.getShopId())) { + e.setShopTakeaway(shopTakeawa); + } } - + }); + } + } + result.put("shop", shop); + } + Worker worker = workerService.findByUserId(user.getId()); + //更新配送员 推送id + if(worker!= null){ + worker.setClientId(clientId); + workerService.update(worker); + String oldRegion = worker.getRegion(); + WorkerRedisVo workerRedisVo = StrUtil.isNotBlank(oldRegion) ? workerRedisCacheUtil.get(oldRegion, worker.getWorkerId()) : null; + if(workerRedisVo != null){ + workerRedisVo.setWorker(worker); + if (Objects.equals(oldRegion, worker.getRegion())) { + workerRedisCacheUtil.update(worker.getRegion(), workerRedisVo); + } else { + workerRedisCacheUtil.remove(oldRegion, worker.getWorkerId()); + if (StrUtil.isNotBlank(worker.getRegion())) { + workerRedisCacheUtil.put(worker.getRegion(), workerRedisVo); } } - result.put("shopList", shopUsers); - } else { - return ResultUtil.error("店铺未开通!"); } } + if(worker!= null && worker.getGetPushOrder() != null && worker.getGetPushOrder() == 1){ + final List workerRelaPriceList = workerRelaPriceService.selectByWorkerId(worker.getWorkerId()); + List workerRelaPriceListWaimai = new ArrayList<>(); + List workerRelaPriceListKuaidi = new ArrayList<>(); + if(workerRelaPriceList != null && workerRelaPriceList.size() > 0){ + workerRelaPriceList.forEach(workerRelaPrice -> { + if(workerRelaPrice.getGetPushOrder() == 1){ + if(workerRelaPrice.getOrderType() == 0 ){ + workerRelaPriceListWaimai.add(workerRelaPrice); + }else{ + workerRelaPriceListKuaidi.add(workerRelaPrice); + } + } + }); + } + if(workerRelaPriceListWaimai.size() > 0){ + result.put("waimaiData", workerRelaPriceListWaimai); + } + if(workerRelaPriceListKuaidi.size() > 0){ + result.put("kuaidiData", workerRelaPriceListKuaidi); + } + } + result.put("worker", worker); return ResultUtil.data(result); } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/entity/BillingUsageRecord.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/entity/BillingUsageRecord.java new file mode 100644 index 00000000..c576a3d1 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/entity/BillingUsageRecord.java @@ -0,0 +1,32 @@ +package cc.hiver.mall.billing.entity; + +import cc.hiver.core.common.utils.SnowFlakeUtil; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDate; +import java.util.Date; + +@Data +@TableName("billing_usage_record") +public class BillingUsageRecord { + + @TableId + private String id = SnowFlakeUtil.nextId().toString(); + + private String recordType; + private String regionId; + private LocalDate billingDate; + private Date occurTime; + private String provider; + private String usageType; + private String bizType; + private String bizId; + private String userId; + private String receiveMobile; + private String resultStatus; + private String requestId; + private String detail; + private Date createTime; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mapper/BillingUsageRecordMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mapper/BillingUsageRecordMapper.java new file mode 100644 index 00000000..af18950a --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mapper/BillingUsageRecordMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.billing.mapper; + +import cc.hiver.mall.billing.entity.BillingUsageRecord; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface BillingUsageRecordMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordConsumer.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordConsumer.java new file mode 100644 index 00000000..21b56f86 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordConsumer.java @@ -0,0 +1,50 @@ +package cc.hiver.mall.billing.mq; + +import cc.hiver.mall.billing.entity.BillingUsageRecord; +import cc.hiver.mall.billing.mapper.BillingUsageRecordMapper; +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.ZoneId; +import java.util.Date; + +@Slf4j +@Component +public class BillingRecordConsumer { + + @Autowired + private BillingUsageRecordMapper billingUsageRecordMapper; + + @RabbitListener(queues = BillingRecordMqConfig.BILLING_RECORD_QUEUE) + public void handleBillingRecord(String messageBody) { + try { + BillingRecordEvent event = JSON.parseObject(messageBody, BillingRecordEvent.class); + if (event == null || event.getRecordType() == null) { + log.warn("计费记录MQ消息为空或缺少类型,body={}", messageBody); + return; + } + Date occurTime = event.getOccurTime() == null ? new Date() : event.getOccurTime(); + BillingUsageRecord record = new BillingUsageRecord(); + record.setRecordType(event.getRecordType()); + record.setRegionId(event.getRegionId()); + record.setBillingDate(occurTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()); + record.setOccurTime(occurTime); + record.setProvider(event.getProvider()); + record.setUsageType(event.getUsageType()); + record.setBizType(event.getBizType()); + record.setBizId(event.getBizId()); + record.setUserId(event.getUserId()); + record.setReceiveMobile(event.getReceiveMobile()); + record.setResultStatus(event.getResultStatus()); + record.setRequestId(event.getRequestId()); + record.setDetail(event.getDetail()); + record.setCreateTime(new Date()); + billingUsageRecordMapper.insert(record); + } catch (Exception e) { + log.error("计费记录MQ处理失败,body={}", messageBody, e); + } + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordEvent.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordEvent.java new file mode 100644 index 00000000..c6704885 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordEvent.java @@ -0,0 +1,26 @@ +package cc.hiver.mall.billing.mq; + +import lombok.Data; + +import java.util.Date; + +@Data +public class BillingRecordEvent { + + public static final String TYPE_SMS = "sms"; + public static final String TYPE_CONTENT_AUDIT = "content_audit"; + public static final String TYPE_REAL_NAME_AUTH = "real_name_auth"; + + private String recordType; + private String regionId; + private Date occurTime; + private String provider; + private String usageType; + private String bizType; + private String bizId; + private String userId; + private String receiveMobile; + private String resultStatus; + private String requestId; + private String detail; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordMqConfig.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordMqConfig.java new file mode 100644 index 00000000..8b058b36 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordMqConfig.java @@ -0,0 +1,31 @@ +package cc.hiver.mall.billing.mq; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class BillingRecordMqConfig { + + public static final String BILLING_RECORD_EXCHANGE = "billing.record.direct.exchange"; + public static final String BILLING_RECORD_QUEUE = "billing.record.queue"; + public static final String BILLING_RECORD_ROUTING = "billing.record.routing"; + + @Bean + public DirectExchange billingRecordExchange() { + return new DirectExchange(BILLING_RECORD_EXCHANGE, true, false); + } + + @Bean + public Queue billingRecordQueue() { + return new Queue(BILLING_RECORD_QUEUE, true); + } + + @Bean + public Binding bindingBillingRecordQueue() { + return BindingBuilder.bind(billingRecordQueue()).to(billingRecordExchange()).with(BILLING_RECORD_ROUTING); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordProducer.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordProducer.java new file mode 100644 index 00000000..5050588d --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordProducer.java @@ -0,0 +1,79 @@ +package cc.hiver.mall.billing.mq; + +import cn.hutool.core.text.CharSequenceUtil; +import com.alibaba.fastjson.JSON; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Slf4j +@Component +public class BillingRecordProducer { + + private static final int DETAIL_MAX_LENGTH = 500; + + @Autowired + private RabbitTemplate rabbitTemplate; + + public void recordSms(String regionId, String smsType, String receiveMobile, String bizType, String bizId) { + BillingRecordEvent event = baseEvent(BillingRecordEvent.TYPE_SMS, regionId, "sms", smsType, bizType, bizId, null); + event.setReceiveMobile(receiveMobile); + event.setResultStatus("sent"); + send(event); + } + + public void recordContentAudit(String regionId, String auditType, String bizType, String bizId, + String userId, String resultStatus, String requestId, String detail) { + BillingRecordEvent event = baseEvent(BillingRecordEvent.TYPE_CONTENT_AUDIT, regionId, "aliyun", auditType, bizType, bizId, userId); + event.setResultStatus(resultStatus); + event.setRequestId(requestId); + event.setDetail(limit(detail)); + send(event); + } + + public void recordRealNameAuth(String regionId, String userId, String resultStatus, String requestId, String detail) { + BillingRecordEvent event = baseEvent(BillingRecordEvent.TYPE_REAL_NAME_AUTH, regionId, + "aliyun_three_element", "three_element", "ie_real_name_auth", null, userId); + event.setResultStatus(resultStatus); + event.setRequestId(requestId); + event.setDetail(limit(detail)); + send(event); + } + + private BillingRecordEvent baseEvent(String recordType, String regionId, String provider, + String usageType, String bizType, String bizId, String userId) { + BillingRecordEvent event = new BillingRecordEvent(); + event.setRecordType(recordType); + event.setRegionId(regionId); + event.setOccurTime(new Date()); + event.setProvider(provider); + event.setUsageType(usageType); + event.setBizType(bizType); + event.setBizId(bizId); + event.setUserId(userId); + return event; + } + + private void send(BillingRecordEvent event) { + if (event == null) { + return; + } + try { + rabbitTemplate.convertAndSend(BillingRecordMqConfig.BILLING_RECORD_EXCHANGE, + BillingRecordMqConfig.BILLING_RECORD_ROUTING, JSON.toJSONString(event)); + } catch (Exception e) { + log.warn("计费记录MQ发送失败,不影响主流程 recordType={}, regionId={}, usageType={}", + event.getRecordType(), event.getRegionId(), event.getUsageType(), e); + } + } + + private String limit(String value) { + if (CharSequenceUtil.isBlank(value) || value.length() <= DETAIL_MAX_LENGTH) { + return value; + } + return value.substring(0, DETAIL_MAX_LENGTH); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminBillingUsageController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminBillingUsageController.java new file mode 100644 index 00000000..fbcf9fe3 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminBillingUsageController.java @@ -0,0 +1,150 @@ +package cc.hiver.mall.controller; + +import cc.hiver.core.common.utils.ResultUtil; +import cc.hiver.core.common.vo.Result; +import cc.hiver.mall.billing.entity.BillingUsageRecord; +import cc.hiver.mall.billing.mapper.BillingUsageRecordMapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiOperation; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@RestController +@Api(tags = "后台管理系统第三方计费用量接口") +@RequestMapping("/hiver/mall/admin/billingUsage") +public class AdminBillingUsageController { + + @Autowired + private BillingUsageRecordMapper billingUsageRecordMapper; + + @Data + public static class BillingUsageQuery { + @ApiModelProperty(value = "校区/学校ID") + private String regionId; + @ApiModelProperty(value = "记录类型:sms/content_audit/real_name_auth") + private String recordType; + @ApiModelProperty(value = "调用类型:短信模板、text/image/video_submit等") + private String usageType; + @ApiModelProperty(value = "开始日期,格式yyyy-MM-dd") + private String startDate; + @ApiModelProperty(value = "结束日期,格式yyyy-MM-dd") + private String endDate; + @ApiModelProperty(value = "页码") + private int pageNumber = 1; + @ApiModelProperty(value = "每页条数") + private int pageSize = 10; + } + + @Data + public static class BillingUsageSummaryVo { + @ApiModelProperty(value = "校区/学校ID") + private String regionId; + @ApiModelProperty(value = "计费日期") + private LocalDate billingDate; + @ApiModelProperty(value = "记录类型") + private String recordType; + @ApiModelProperty(value = "调用类型") + private String usageType; + @ApiModelProperty(value = "用量次数") + private Long usageCount; + } + + @PostMapping("/summaryList") + @ApiOperation(value = "按校区/日期/类型查询第三方计费用量汇总") + public Result> summaryList(@RequestBody BillingUsageQuery query) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.select("region_id", "billing_date", "record_type", "usage_type", "count(1) as usage_count"); + applyMapConditions(wrapper, query); + wrapper.groupBy("region_id", "billing_date", "record_type", "usage_type"); + wrapper.orderByDesc("billing_date").orderByAsc("region_id", "record_type", "usage_type"); + Page> rowPage = billingUsageRecordMapper.selectMapsPage( + new Page<>(Math.max(query.getPageNumber(), 1), Math.max(query.getPageSize(), 10)), wrapper); + List records = new ArrayList<>(); + for (Map row : rowPage.getRecords()) { + BillingUsageSummaryVo vo = new BillingUsageSummaryVo(); + vo.setRegionId(stringValue(row.get("region_id"))); + vo.setBillingDate(toLocalDate(row.get("billing_date"))); + vo.setRecordType(stringValue(row.get("record_type"))); + vo.setUsageType(stringValue(row.get("usage_type"))); + vo.setUsageCount(longValue(row.get("usage_count"))); + records.add(vo); + } + Page result = new Page<>(rowPage.getCurrent(), rowPage.getSize(), rowPage.getTotal()); + result.setRecords(records); + return new ResultUtil>().setData(result); + } + + @PostMapping("/detailList") + @ApiOperation(value = "查询第三方计费用量明细") + public Result> detailList(@RequestBody BillingUsageQuery query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (StringUtils.isNotBlank(query.getRegionId())) { + wrapper.eq(BillingUsageRecord::getRegionId, query.getRegionId()); + } + if (StringUtils.isNotBlank(query.getRecordType())) { + wrapper.eq(BillingUsageRecord::getRecordType, query.getRecordType()); + } + if (StringUtils.isNotBlank(query.getUsageType())) { + wrapper.eq(BillingUsageRecord::getUsageType, query.getUsageType()); + } + if (StringUtils.isNotBlank(query.getStartDate())) { + wrapper.ge(BillingUsageRecord::getBillingDate, LocalDate.parse(query.getStartDate())); + } + if (StringUtils.isNotBlank(query.getEndDate())) { + wrapper.le(BillingUsageRecord::getBillingDate, LocalDate.parse(query.getEndDate())); + } + wrapper.orderByDesc(BillingUsageRecord::getOccurTime); + Page page = new Page<>(Math.max(query.getPageNumber(), 1), Math.max(query.getPageSize(), 10)); + return new ResultUtil>().setData(billingUsageRecordMapper.selectPage(page, wrapper)); + } + + private void applyMapConditions(QueryWrapper wrapper, BillingUsageQuery query) { + if (StringUtils.isNotBlank(query.getRegionId())) { + wrapper.eq("region_id", query.getRegionId()); + } + if (StringUtils.isNotBlank(query.getRecordType())) { + wrapper.eq("record_type", query.getRecordType()); + } + if (StringUtils.isNotBlank(query.getUsageType())) { + wrapper.eq("usage_type", query.getUsageType()); + } + if (StringUtils.isNotBlank(query.getStartDate())) { + wrapper.ge("billing_date", LocalDate.parse(query.getStartDate())); + } + if (StringUtils.isNotBlank(query.getEndDate())) { + wrapper.le("billing_date", LocalDate.parse(query.getEndDate())); + } + } + + private String stringValue(Object value) { + return value == null ? null : String.valueOf(value); + } + + private Long longValue(Object value) { + if (value instanceof Number) { + return ((Number) value).longValue(); + } + return value == null ? 0L : Long.parseLong(String.valueOf(value)); + } + + private LocalDate toLocalDate(Object value) { + if (value instanceof LocalDate) { + return (LocalDate) value; + } + return value == null ? null : LocalDate.parse(String.valueOf(value)); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminDataStatisticsController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminDataStatisticsController.java new file mode 100644 index 00000000..9da8d939 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminDataStatisticsController.java @@ -0,0 +1,299 @@ +package cc.hiver.mall.controller; + +import cc.hiver.core.common.utils.ResultUtil; +import cc.hiver.core.common.vo.Result; +import cc.hiver.mall.billing.entity.BillingUsageRecord; +import cc.hiver.mall.billing.mapper.BillingUsageRecordMapper; +import cc.hiver.mall.dao.mapper.*; +import cc.hiver.mall.entity.*; +import cc.hiver.mall.planet.entity.PlanetPool; +import cc.hiver.mall.planet.mapper.PlanetPoolMapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiModelProperty; +import io.swagger.annotations.ApiOperation; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@RestController +@Api(tags = "后台管理系统数据统计接口") +@RequestMapping("/hiver/mall/admin/dataStatistics") +public class AdminDataStatisticsController { + + private static final BigDecimal WECHAT_FEE_RATE = new BigDecimal("0.006"); + private static final BigDecimal ONE_PERCENT = new BigDecimal("0.01"); + private static final BigDecimal SMS_UNIT_PRICE = new BigDecimal("0.05"); + private static final BigDecimal CONTENT_AUDIT_PACKAGE_PRICE = new BigDecimal("0.05"); + private static final BigDecimal REAL_NAME_UNIT_PRICE = BigDecimal.ONE; + + private static final String REWARD_FREE_ORDER = "免单奖励"; + private static final String REWARD_COLLEGE_RANK = "学院排位赛排名奖励"; + private static final String REWARD_DELIVERY_RANK = "配送排名奖励"; + + @Autowired + private MallOrderMapper mallOrderMapper; + @Autowired + private MallRefundRecordMapper mallRefundRecordMapper; + @Autowired + private MallDeliveryOrderMapper mallDeliveryOrderMapper; + @Autowired + private MallUserCouponMapper mallUserCouponMapper; + @Autowired + private DealingsRecordMapper dealingsRecordMapper; + @Autowired + private PlanetPoolMapper planetPoolMapper; + @Autowired + private BillingUsageRecordMapper billingUsageRecordMapper; + + @Data + public static class DataStatisticsQuery { + @ApiModelProperty(value = "校区/学校ID") + private String regionId; + @ApiModelProperty(value = "开始日期,格式yyyy-MM-dd,默认今日") + private String startDate; + @ApiModelProperty(value = "结束日期,格式yyyy-MM-dd,默认今日") + private String endDate; + } + + @Data + public static class DataStatisticsSummaryVo { + private String startDate; + private String endDate; + private Long orderCount; + private BigDecimal grossFlowAmount; + private BigDecimal effectiveFlowAmount; + private BigDecimal koiDiscountAmount; + private BigDecimal onTimeRate; + private BigDecimal refundAmount; + private BigDecimal afterSaleAmount; + private BigDecimal couponDiscountAmount; + private BigDecimal customerAverageAmount; + private BigDecimal actualIncomeAmount; + private BigDecimal headquartersDeductionAmount; + private BigDecimal deliveryFeeAmount; + private Long smsCount; + private Long contentAuditCount; + private Long realNameAuthCount; + private BigDecimal smsCostAmount; + private BigDecimal contentAuditCostAmount; + private BigDecimal realNameAuthCostAmount; + private BigDecimal collegeRankRewardAmount; + private BigDecimal drawPoolAmount; + private BigDecimal deliveryRankRewardAmount; + private Long orderShopCount; + private Long orderUserCount; + } + + @PostMapping("/summary") + @ApiOperation(value = "按校区和日期范围获取数据统计汇总") + public Result summary(@RequestBody DataStatisticsQuery query) { + DateRange range = resolveDateRange(query); + String regionId = query == null ? null : query.getRegionId(); + + List orders = mallOrderMapper.selectList(new LambdaQueryWrapper() + .eq(StringUtils.isNotBlank(regionId), MallOrder::getRegionId, regionId) + .ge(MallOrder::getPayTime, range.start) + .le(MallOrder::getPayTime, range.end) + .notIn(MallOrder::getStatus, 0, 6)); + + List refunds = mallRefundRecordMapper.selectList(new LambdaQueryWrapper() + .eq(StringUtils.isNotBlank(regionId), MallRefundRecord::getRegionId, regionId) + .ge(MallRefundRecord::getSuccessTime, range.start) + .le(MallRefundRecord::getSuccessTime, range.end) + .in(MallRefundRecord::getStatus, 1, 4)); + + List deliveries = mallDeliveryOrderMapper.selectList(new LambdaQueryWrapper() + .eq(StringUtils.isNotBlank(regionId), MallDeliveryOrder::getRegionId, regionId) + .ge(MallDeliveryOrder::getFinishTime, range.start) + .le(MallDeliveryOrder::getFinishTime, range.end) + .isNotNull(MallDeliveryOrder::getFinishTime)); + + List coupons = mallUserCouponMapper.selectList(new LambdaQueryWrapper() + .eq(StringUtils.isNotBlank(regionId), MallUserCoupon::getRegionId, regionId) + .in(MallUserCoupon::getStatus, 1,2) + .ge(MallUserCoupon::getUseTime, range.start) + .le(MallUserCoupon::getUseTime, range.end)); + + List rewardRecords = dealingsRecordMapper.selectList(new LambdaQueryWrapper() + .eq(StringUtils.isNotBlank(regionId), DealingsRecord::getRegionId, regionId) + .ge(DealingsRecord::getDealingsTime, range.start) + .le(DealingsRecord::getDealingsTime, range.end) + .in(DealingsRecord::getDealingsWay, REWARD_FREE_ORDER, REWARD_COLLEGE_RANK, REWARD_DELIVERY_RANK)); + + List pools = planetPoolMapper.selectList(new LambdaQueryWrapper() + .eq(StringUtils.isNotBlank(regionId), PlanetPool::getRegionId, regionId) + .ge(PlanetPool::getDrawTime, range.start) + .le(PlanetPool::getDrawTime, range.end)); + + List billingRecords = billingUsageRecordMapper.selectList(new LambdaQueryWrapper() + .eq(StringUtils.isNotBlank(regionId), BillingUsageRecord::getRegionId, regionId) + .ge(BillingUsageRecord::getBillingDate, range.startDate) + .le(BillingUsageRecord::getBillingDate, range.endDate)); + + DataStatisticsSummaryVo vo = new DataStatisticsSummaryVo(); + vo.setStartDate(range.startDate.toString()); + vo.setEndDate(range.endDate.toString()); + + BigDecimal goodsAndPackage = BigDecimal.ZERO; + BigDecimal deliveryFee = BigDecimal.ZERO; + Set shopIds = new HashSet<>(); + Set userIds = new HashSet<>(); + for (MallOrder order : orders) { + goodsAndPackage = goodsAndPackage.add(amount(order.getGoodsAmount())).add(amount(order.getPackageFee())); + deliveryFee = deliveryFee.add(amount(order.getDeliveryFee())); + if (StringUtils.isNotBlank(order.getShopId())) { + shopIds.add(order.getShopId()); + } + if (StringUtils.isNotBlank(order.getUserId())) { + userIds.add(order.getUserId()); + } + } + + BigDecimal deliveryFeeAmount = subtractWechatFee(deliveryFee); + BigDecimal customerAverageBase = subtractWechatFee(goodsAndPackage); + BigDecimal grossFlow = subtractWechatFee(goodsAndPackage.add(deliveryFee)); + BigDecimal refundAmount = sumRefundAmount(refunds, 1); + BigDecimal afterSaleAmount = sumRefundAmount(refunds, 4); + BigDecimal effectiveFlow = grossFlow.subtract(refundAmount).subtract(afterSaleAmount).subtract(deliveryFeeAmount); + BigDecimal headquartersDeduction = effectiveFlow.multiply(ONE_PERCENT); + + BigDecimal koiDiscount = sumDealingsAmount(rewardRecords, REWARD_FREE_ORDER); + BigDecimal collegeRankReward = sumDealingsAmount(rewardRecords, REWARD_COLLEGE_RANK); + BigDecimal deliveryRankReward = sumDealingsAmount(rewardRecords, REWARD_DELIVERY_RANK); + BigDecimal couponDiscount = coupons.stream() + .map(MallUserCoupon::getDiscountAmount) + .map(this::amount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal drawPoolAmount = pools.stream() + .map(PlanetPool::getPoolAmount) + .map(this::amount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + long smsCount = billingRecords.stream().filter(item -> "sms".equals(item.getRecordType())).count(); + long contentAuditCount = billingRecords.stream().filter(item -> "content_audit".equals(item.getRecordType())).count(); + long realNameAuthCount = billingRecords.stream().filter(item -> "real_name_auth".equals(item.getRecordType())).count(); + BigDecimal smsCost = BigDecimal.valueOf(smsCount).multiply(SMS_UNIT_PRICE); + BigDecimal contentAuditCost = BigDecimal.valueOf((contentAuditCount + 99) / 100).multiply(CONTENT_AUDIT_PACKAGE_PRICE); + BigDecimal realNameCost = BigDecimal.valueOf(realNameAuthCount).multiply(REAL_NAME_UNIT_PRICE); + BigDecimal actualIncome = effectiveFlow + .subtract(headquartersDeduction) + .subtract(koiDiscount) + .subtract(collegeRankReward) + .subtract(drawPoolAmount) + .subtract(deliveryRankReward) + .subtract(smsCost) + .subtract(contentAuditCost) + .subtract(realNameCost); + + vo.setOrderCount((long) orders.size()); + vo.setGrossFlowAmount(money(grossFlow)); + vo.setDeliveryFeeAmount(money(deliveryFeeAmount)); + vo.setRefundAmount(money(refundAmount)); + vo.setAfterSaleAmount(money(afterSaleAmount)); + vo.setEffectiveFlowAmount(money(effectiveFlow)); + vo.setKoiDiscountAmount(money(koiDiscount)); + vo.setCouponDiscountAmount(money(couponDiscount)); + vo.setOnTimeRate(onTimeRate(deliveries)); + vo.setCustomerAverageAmount(orders.isEmpty() ? BigDecimal.ZERO : money(customerAverageBase.divide(BigDecimal.valueOf(orders.size()), 4, RoundingMode.HALF_UP))); + vo.setHeadquartersDeductionAmount(money(headquartersDeduction)); + vo.setActualIncomeAmount(money(actualIncome)); + vo.setSmsCount(smsCount); + vo.setContentAuditCount(contentAuditCount); + vo.setRealNameAuthCount(realNameAuthCount); + vo.setSmsCostAmount(money(smsCost)); + vo.setContentAuditCostAmount(money(contentAuditCost)); + vo.setRealNameAuthCostAmount(money(realNameCost)); + vo.setCollegeRankRewardAmount(money(collegeRankReward)); + vo.setDrawPoolAmount(money(drawPoolAmount)); + vo.setDeliveryRankRewardAmount(money(deliveryRankReward)); + vo.setOrderShopCount((long) shopIds.size()); + vo.setOrderUserCount((long) userIds.size()); + return new ResultUtil().setData(vo); + } + + private BigDecimal sumRefundAmount(List refunds, int status) { + return refunds.stream() + .filter(item -> item.getStatus() != null && item.getStatus() == status) + .map(MallRefundRecord::getRefundAmount) + .map(this::amount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private BigDecimal sumDealingsAmount(List records, String dealingsWay) { + return records.stream() + .filter(item -> dealingsWay.equals(item.getDealingsWay())) + .map(DealingsRecord::getAmount) + .map(this::amount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private BigDecimal onTimeRate(List deliveries) { + if (deliveries == null || deliveries.isEmpty()) { + return BigDecimal.ZERO; + } + long onTime = deliveries.stream() + .filter(item -> item.getFinishTime() != null && item.getMustFinishTime() != null + && !item.getFinishTime().after(item.getMustFinishTime())) + .count(); + return BigDecimal.valueOf(onTime) + .multiply(new BigDecimal("100")) + .divide(BigDecimal.valueOf(deliveries.size()), 2, RoundingMode.HALF_UP); + } + + private BigDecimal subtractWechatFee(BigDecimal value) { + return amount(value).multiply(BigDecimal.ONE.subtract(WECHAT_FEE_RATE)); + } + + private BigDecimal amount(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + + private BigDecimal money(BigDecimal value) { + return amount(value).setScale(2, RoundingMode.HALF_UP); + } + + private DateRange resolveDateRange(DataStatisticsQuery query) { + LocalDate today = LocalDate.now(); + LocalDate startDate = parseDate(query == null ? null : query.getStartDate(), today); + LocalDate endDate = parseDate(query == null ? null : query.getEndDate(), startDate); + if (endDate.isBefore(startDate)) { + endDate = startDate; + } + DateRange range = new DateRange(); + range.startDate = startDate; + range.endDate = endDate; + range.start = Date.from(LocalDateTime.of(startDate, LocalTime.MIN).atZone(ZoneId.systemDefault()).toInstant()); + range.end = Date.from(LocalDateTime.of(endDate, LocalTime.MAX).atZone(ZoneId.systemDefault()).toInstant()); + return range; + } + + private LocalDate parseDate(String value, LocalDate defaultValue) { + if (StringUtils.isBlank(value)) { + return defaultValue; + } + return LocalDate.parse(value); + } + + private static class DateRange { + private LocalDate startDate; + private LocalDate endDate; + private Date start; + private Date end; + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSettlementController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSettlementController.java index b2e238f7..d3f3bd18 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSettlementController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSettlementController.java @@ -9,6 +9,7 @@ import cc.hiver.mall.service.ShopService; import cc.hiver.mall.service.mybatis.MallOrderService; import cc.hiver.mall.service.mybatis.MallSettlementRecordService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import io.swagger.annotations.Api; import io.swagger.annotations.ApiModelProperty; @@ -23,8 +24,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -98,6 +97,8 @@ public class AdminSettlementController { public static class SummaryQuery { @ApiModelProperty(value = "区域ID") private String regionId; + @ApiModelProperty(value = "商家ID") + private String shopId; @ApiModelProperty(value = "结算日期,格式yyyy-MM-dd") private String settlementDate; @ApiModelProperty(value = "结算状态 0待结算 1已结算") @@ -151,76 +152,65 @@ public class AdminSettlementController { return ResultUtil.error("区域ID或日期不能为空"); } - LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); - qw.eq(MallSettlementRecord::getRegionId, query.getRegionId()); - qw.eq(MallSettlementRecord::getStatus, query.getStatus() == null ? 0 : query.getStatus()); + QueryWrapper qw = new QueryWrapper<>(); + qw.select("shop_id", + "COALESCE(SUM(base_amount), 0) AS total_base_amount", + "COALESCE(SUM(commission_amount), 0) AS total_commission_amount", + "COALESCE(SUM(settlement_amount), 0) AS total_settlement_amount", + "COUNT(1) AS record_count", + "SUM(CASE WHEN type = 2 THEN 0 ELSE 1 END) AS positive_count", + "SUM(CASE WHEN type = 2 THEN 1 ELSE 0 END) AS negative_count", + "COALESCE(SUM(CASE WHEN type = 2 THEN 0 ELSE settlement_amount END), 0) AS positive_settlement_amount", + "COALESCE(SUM(CASE WHEN type = 2 THEN settlement_amount ELSE 0 END), 0) AS negative_settlement_amount"); + qw.eq("region_id", query.getRegionId()); + if (StringUtils.isNotBlank(query.getShopId())) { + qw.eq("shop_id", query.getShopId()); + } + qw.eq("status", query.getStatus() == null ? 0 : query.getStatus()); qw.apply("DATE(create_time) = {0}", query.getSettlementDate()); + qw.groupBy("shop_id"); + qw.orderByAsc("shop_id"); - List list = mallSettlementRecordService.list(qw); - - // Group by shopId - Map> map = list.stream().collect(Collectors.groupingBy(MallSettlementRecord::getShopId)); - List shopIds = map.keySet().stream() + Page> rowPage = mallSettlementRecordService.pageMaps( + new Page<>(pageNumber(query), pageSize(query)), qw); + List shopIds = rowPage.getRecords().stream() + .map(row -> stringValue(row.get("shop_id"))) .filter(StringUtils::isNotBlank) .collect(Collectors.toList()); Map shopMap = shopIds.isEmpty() ? new java.util.HashMap<>() : shopService.getRepository().findAllById(shopIds).stream() .collect(Collectors.toMap(Shop::getId, Function.identity(), (a, b) -> a)); - List result = new ArrayList<>(); - for (Map.Entry> entry : map.entrySet()) { - String shopId = entry.getKey(); - List shopRecords = entry.getValue(); - + List records = new java.util.ArrayList<>(); + for (Map row : rowPage.getRecords()) { + String shopId = stringValue(row.get("shop_id")); SettlementSummaryVo vo = new SettlementSummaryVo(); vo.setShopId(shopId); - Shop shop = shopMap.get(shopId); if (shop != null) { vo.setShopName(shop.getShopName()); } - - BigDecimal totalBase = BigDecimal.ZERO; - BigDecimal totalComm = BigDecimal.ZERO; - BigDecimal totalSettlement = BigDecimal.ZERO; - BigDecimal positiveSettlement = BigDecimal.ZERO; - BigDecimal negativeSettlement = BigDecimal.ZERO; - int positiveCount = 0; - int negativeCount = 0; - - for (MallSettlementRecord r : shopRecords) { - if (r.getBaseAmount() != null) totalBase = totalBase.add(r.getBaseAmount()); - if (r.getCommissionAmount() != null) totalComm = totalComm.add(r.getCommissionAmount()); - if (r.getSettlementAmount() != null) totalSettlement = totalSettlement.add(r.getSettlementAmount()); - if (r.getType() != null && r.getType() == 2) { - negativeCount++; - if (r.getSettlementAmount() != null) negativeSettlement = negativeSettlement.add(r.getSettlementAmount()); - } else { - positiveCount++; - if (r.getSettlementAmount() != null) positiveSettlement = positiveSettlement.add(r.getSettlementAmount()); - } - } vo.setBillTime(query.getSettlementDate()); - vo.setTotalBaseAmount(totalBase); - vo.setTotalCommissionAmount(totalComm); - vo.setTotalSettlementAmount(totalSettlement); - vo.setRecordCount(shopRecords.size()); - vo.setPositiveCount(positiveCount); - vo.setNegativeCount(negativeCount); - vo.setPositiveSettlementAmount(positiveSettlement); - vo.setNegativeSettlementAmount(negativeSettlement); + vo.setTotalBaseAmount(decimalValue(row.get("total_base_amount"))); + vo.setTotalCommissionAmount(decimalValue(row.get("total_commission_amount"))); + vo.setTotalSettlementAmount(decimalValue(row.get("total_settlement_amount"))); + vo.setRecordCount(intValue(row.get("record_count"))); + vo.setPositiveCount(intValue(row.get("positive_count"))); + vo.setNegativeCount(intValue(row.get("negative_count"))); + vo.setPositiveSettlementAmount(decimalValue(row.get("positive_settlement_amount"))); + vo.setNegativeSettlementAmount(decimalValue(row.get("negative_settlement_amount"))); vo.setStatus(query.getStatus() == null ? 0 : query.getStatus()); if (vo.getStatus() == 0) { - vo.setPendingSettlementAmount(totalSettlement); + vo.setPendingSettlementAmount(vo.getTotalSettlementAmount()); vo.setConfirmedSettlementAmount(BigDecimal.ZERO); } else { vo.setPendingSettlementAmount(BigDecimal.ZERO); - vo.setConfirmedSettlementAmount(totalSettlement); + vo.setConfirmedSettlementAmount(vo.getTotalSettlementAmount()); } - - result.add(vo); + records.add(vo); } - result.sort(Comparator.comparing(vo -> StringUtils.defaultString(vo.getShopName(), vo.getShopId()))); - return new ResultUtil>().setData(buildPage(result, query)); + Page result = new Page<>(rowPage.getCurrent(), rowPage.getSize(), rowPage.getTotal()); + result.setRecords(records); + return new ResultUtil>().setData(result); } @Data @@ -248,43 +238,52 @@ public class AdminSettlementController { @PostMapping("/dateSummaryList") @ApiOperation(value = "按日期获取结算汇总") public Result> dateSummaryList(@RequestBody SummaryQuery query) { - if (StringUtils.isBlank(query.getRegionId())) { - return ResultUtil.error("区域ID不能为空"); + if (StringUtils.isBlank(query.getRegionId()) && StringUtils.isBlank(query.getShopId())) { + return ResultUtil.error("区域ID或商家ID不能为空"); } - LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); - qw.eq(MallSettlementRecord::getRegionId, query.getRegionId()); - qw.eq(MallSettlementRecord::getStatus, query.getStatus() == null ? 0 : query.getStatus()); + QueryWrapper qw = new QueryWrapper<>(); + qw.select("DATE(create_time) AS bill_time", + "COUNT(DISTINCT shop_id) AS shop_count", + "COUNT(1) AS record_count", + "COALESCE(SUM(base_amount), 0) AS total_base_amount", + "COALESCE(SUM(commission_amount), 0) AS total_commission_amount", + "COALESCE(SUM(settlement_amount), 0) AS total_settlement_amount", + "COALESCE(SUM(CASE WHEN type = 2 THEN 0 ELSE settlement_amount END), 0) AS positive_settlement_amount", + "COALESCE(SUM(CASE WHEN type = 2 THEN settlement_amount ELSE 0 END), 0) AS negative_settlement_amount"); + if (StringUtils.isNotBlank(query.getRegionId())) { + qw.eq("region_id", query.getRegionId()); + } + if (StringUtils.isNotBlank(query.getShopId())) { + qw.eq("shop_id", query.getShopId()); + } + qw.eq("status", query.getStatus() == null ? 0 : query.getStatus()); if (StringUtils.isNotBlank(query.getStartDate())) { qw.apply("DATE(create_time) >= {0}", query.getStartDate()); } if (StringUtils.isNotBlank(query.getEndDate())) { qw.apply("DATE(create_time) <= {0}", query.getEndDate()); } - List list = mallSettlementRecordService.list(qw); - Map> byDate = list.stream() - .collect(Collectors.groupingBy(r -> new java.text.SimpleDateFormat("yyyy-MM-dd").format(r.getCreateTime()))); - List result = new ArrayList<>(); - for (Map.Entry> entry : byDate.entrySet()) { + qw.groupBy("DATE(create_time)"); + qw.orderByDesc("bill_time"); + Page> rowPage = mallSettlementRecordService.pageMaps( + new Page<>(pageNumber(query), pageSize(query)), qw); + List records = new java.util.ArrayList<>(); + for (Map row : rowPage.getRecords()) { DateSummaryVo vo = new DateSummaryVo(); - vo.setBillTime(entry.getKey()); + vo.setBillTime(stringValue(row.get("bill_time"))); vo.setStatus(query.getStatus() == null ? 0 : query.getStatus()); - vo.setShopCount((int) entry.getValue().stream().map(MallSettlementRecord::getShopId).distinct().count()); - vo.setRecordCount(entry.getValue().size()); - fillAmounts(vo, entry.getValue()); - result.add(vo); + vo.setShopCount(intValue(row.get("shop_count"))); + vo.setRecordCount(intValue(row.get("record_count"))); + vo.setTotalBaseAmount(decimalValue(row.get("total_base_amount"))); + vo.setTotalCommissionAmount(decimalValue(row.get("total_commission_amount"))); + vo.setTotalSettlementAmount(decimalValue(row.get("total_settlement_amount"))); + vo.setPositiveSettlementAmount(decimalValue(row.get("positive_settlement_amount"))); + vo.setNegativeSettlementAmount(decimalValue(row.get("negative_settlement_amount"))); + records.add(vo); } - result.sort(Comparator.comparing(DateSummaryVo::getBillTime).reversed()); - return new ResultUtil>().setData(buildPage(result, query)); - } - - private Page buildPage(List list, SummaryQuery query) { - int pageNumber = Math.max(query.getPageNumber(), 1); - int pageSize = Math.max(query.getPageSize(), 10); - int fromIndex = Math.min((pageNumber - 1) * pageSize, list.size()); - int toIndex = Math.min(fromIndex + pageSize, list.size()); - Page page = new Page<>(pageNumber, pageSize, list.size()); - page.setRecords(list.subList(fromIndex, toIndex)); - return page; + Page result = new Page<>(rowPage.getCurrent(), rowPage.getSize(), rowPage.getTotal()); + result.setRecords(records); + return new ResultUtil>().setData(result); } @Data @@ -293,11 +292,13 @@ public class AdminSettlementController { private String settlementDate; private String shopId; private Integer status; + private int pageNumber = 1; + private int pageSize = 10; } @PostMapping("/detailList") @ApiOperation(value = "按日期/商家获取结算明细") - public Result> detailList(@RequestBody DetailQuery query) { + public Result> detailList(@RequestBody DetailQuery query) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); if (StringUtils.isNotBlank(query.getRegionId())) { qw.eq(MallSettlementRecord::getRegionId, query.getRegionId()); @@ -312,7 +313,9 @@ public class AdminSettlementController { qw.apply("DATE(create_time) = {0}", query.getSettlementDate()); } qw.orderByDesc(MallSettlementRecord::getCreateTime); - List list = mallSettlementRecordService.list(qw); + Page page = mallSettlementRecordService.page( + new Page<>(Math.max(query.getPageNumber(), 1), Math.max(query.getPageSize(), 10)), qw); + List list = page.getRecords(); List shopIds = list.stream() .map(MallSettlementRecord::getShopId) .filter(StringUtils::isNotBlank) @@ -340,32 +343,36 @@ public class AdminSettlementController { record.setPayTime(order.getPayTime()); } } - return new ResultUtil>().setData(list); + return new ResultUtil>().setData(page); } - private void fillAmounts(DateSummaryVo vo, List records) { - BigDecimal totalBase = BigDecimal.ZERO; - BigDecimal totalComm = BigDecimal.ZERO; - BigDecimal totalSettlement = BigDecimal.ZERO; - BigDecimal positiveSettlement = BigDecimal.ZERO; - BigDecimal negativeSettlement = BigDecimal.ZERO; - for (MallSettlementRecord r : records) { - if (r.getBaseAmount() != null) totalBase = totalBase.add(r.getBaseAmount()); - if (r.getCommissionAmount() != null) totalComm = totalComm.add(r.getCommissionAmount()); - if (r.getSettlementAmount() != null) { - totalSettlement = totalSettlement.add(r.getSettlementAmount()); - if (r.getType() != null && r.getType() == 2) { - negativeSettlement = negativeSettlement.add(r.getSettlementAmount()); - } else { - positiveSettlement = positiveSettlement.add(r.getSettlementAmount()); - } - } + private int pageNumber(SummaryQuery query) { + return query == null ? 1 : Math.max(query.getPageNumber(), 1); + } + + private int pageSize(SummaryQuery query) { + return query == null ? 10 : Math.max(query.getPageSize(), 10); + } + + private String stringValue(Object value) { + return value == null ? null : String.valueOf(value); + } + + private BigDecimal decimalValue(Object value) { + if (value == null) { + return BigDecimal.ZERO; + } + if (value instanceof BigDecimal) { + return (BigDecimal) value; + } + return new BigDecimal(String.valueOf(value)); + } + + private Integer intValue(Object value) { + if (value instanceof Number) { + return ((Number) value).intValue(); } - vo.setTotalBaseAmount(totalBase); - vo.setTotalCommissionAmount(totalComm); - vo.setTotalSettlementAmount(totalSettlement); - vo.setPositiveSettlementAmount(positiveSettlement); - vo.setNegativeSettlementAmount(negativeSettlement); + return value == null ? 0 : Integer.parseInt(String.valueOf(value)); } @Data diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ReturnCommissionController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ReturnCommissionController.java index df70e116..5a48519b 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ReturnCommissionController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ReturnCommissionController.java @@ -33,6 +33,7 @@ import java.math.BigDecimal; import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; @Slf4j @RestController @@ -42,6 +43,8 @@ import java.util.concurrent.TimeUnit; public class ReturnCommissionController { private static final long WITHDRAW_REQUEST_LOCK_MINUTES = 30L; private static final long WITHDRAW_ACCOUNT_LOCK_SECONDS = 120L; + private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$"); + private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"); @Autowired private ReturnCommissionService returnCommissionService; @@ -163,8 +166,8 @@ public class ReturnCommissionController { } //支付宝户名(姓名) final String name = returnCommission.getAliName(); //shop.getAliName(); - //支付宝账号(手机号) - final String phoneNumber = returnCommission.getAliAccount();// shop.getAliAccount(); + //支付宝账号(手机号或邮箱) + final String aliAccount = returnCommission.getAliAccount();// shop.getAliAccount(); // 提现金额 final BigDecimal commission = returnCommission.getCommission(); // 店铺当前返佣余额 @@ -177,7 +180,7 @@ public class ReturnCommissionController { }*/ try { keepRequestLock = true; - final AlipayFundTransUniTransferResponse payResult = AliPayNewUtil.pay(phoneNumber, name, String.valueOf(commission), returnCommission.getId()); + final AlipayFundTransUniTransferResponse payResult = AliPayNewUtil.pay(aliAccount, name, String.valueOf(commission), returnCommission.getId()); if ("SUCCESS".equals(payResult.getStatus())) { returnCommission.setStatus("1"); aliResult = "支付宝转账成功"; @@ -202,20 +205,23 @@ public class ReturnCommissionController { dealingsRecordService.save(dealingsRecord); } else { returnCommission.setStatus("2"); - returnCommission.setFailMessage("支付宝转账失败"); - returnCommission.setRemark(payResult.getSubMsg()); - aliResult = "支付宝转账失败,请联系管理员!"; + aliResult = buildAlipayFailReason(payResult); + returnCommission.setFailMessage(aliResult); + returnCommission.setRemark(aliResult); } } catch (AlipayApiException e) { returnCommission.setStatus("2"); - returnCommission.setFailMessage("支付宝转账失败"); - returnCommission.setRemark(e.getErrMsg()); - aliResult = "支付宝转账失败,请联系管理员!"; + aliResult = buildAlipayExceptionReason(e); + returnCommission.setFailMessage(aliResult); + returnCommission.setRemark(aliResult); } //2.修改提现记录 result = returnCommissionService.save(returnCommission); if (result) { keepRequestLock = true; + if ("2".equals(returnCommission.getStatus())) { + return ResultUtil.error(aliResult); + } return ResultUtil.success("处理完成," + aliResult); } else { return ResultUtil.error("修改提现数据失败," + aliResult); @@ -272,8 +278,8 @@ public class ReturnCommissionController { } //支付宝户名(姓名) final String name = returnCommission.getAliName(); //shop.getAliName(); - //支付宝账号(手机号) - final String phoneNumber = returnCommission.getAliAccount();// shop.getAliAccount(); + //支付宝账号(手机号或邮箱) + final String aliAccount = returnCommission.getAliAccount();// shop.getAliAccount(); // 提现金额 final BigDecimal commission = returnCommission.getCommission(); // 店铺当前返佣余额 @@ -286,7 +292,7 @@ public class ReturnCommissionController { }*/ try { keepRequestLock = true; - final AlipayFundTransUniTransferResponse payResult = AliPayNewUtil.pay(phoneNumber, name, String.valueOf(commission), returnCommission.getId()); + final AlipayFundTransUniTransferResponse payResult = AliPayNewUtil.pay(aliAccount, name, String.valueOf(commission), returnCommission.getId()); if ("SUCCESS".equals(payResult.getStatus())) { returnCommission.setStatus("1"); aliResult = "支付宝转账成功"; @@ -311,20 +317,23 @@ public class ReturnCommissionController { dealingsRecordService.save(dealingsRecord); } else { returnCommission.setStatus("2"); - returnCommission.setFailMessage("支付宝转账失败"); - returnCommission.setRemark(payResult.getSubMsg()); - aliResult = "支付宝转账失败,请联系管理员!"; + aliResult = buildAlipayFailReason(payResult); + returnCommission.setFailMessage(aliResult); + returnCommission.setRemark(aliResult); } } catch (AlipayApiException e) { returnCommission.setStatus("2"); - returnCommission.setFailMessage("支付宝转账失败"); - returnCommission.setRemark(e.getErrMsg()); - aliResult = "支付宝转账失败,请联系管理员!"; + aliResult = buildAlipayExceptionReason(e); + returnCommission.setFailMessage(aliResult); + returnCommission.setRemark(aliResult); } //2.修改提现记录 result = returnCommissionService.save(returnCommission); if (result) { keepRequestLock = true; + if ("2".equals(returnCommission.getStatus())) { + return ResultUtil.error(aliResult); + } return ResultUtil.success("处理完成," + aliResult); } else { return ResultUtil.error("修改提现数据失败," + aliResult); @@ -381,8 +390,8 @@ public class ReturnCommissionController { } //支付宝户名(姓名) final String name = returnCommission.getAliName(); //shop.getAliName(); - //支付宝账号(手机号) - final String phoneNumber = returnCommission.getAliAccount();// shop.getAliAccount(); + //支付宝账号(手机号或邮箱) + final String aliAccount = returnCommission.getAliAccount();// shop.getAliAccount(); // 提现金额 final BigDecimal commission = returnCommission.getCommission(); // 店铺当前返佣余额 @@ -395,7 +404,7 @@ public class ReturnCommissionController { }*/ try { keepRequestLock = true; - final AlipayFundTransUniTransferResponse payResult = AliPayNewUtil.pay(phoneNumber, name, String.valueOf(commission), returnCommission.getId()); + final AlipayFundTransUniTransferResponse payResult = AliPayNewUtil.pay(aliAccount, name, String.valueOf(commission), returnCommission.getId()); if ("SUCCESS".equals(payResult.getStatus())) { returnCommission.setStatus("1"); aliResult = "支付宝转账成功"; @@ -420,20 +429,23 @@ public class ReturnCommissionController { dealingsRecordService.save(dealingsRecord); } else { returnCommission.setStatus("2"); - returnCommission.setFailMessage("支付宝转账失败"); - returnCommission.setRemark(payResult.getSubMsg()); - aliResult = "支付宝转账失败,请联系管理员!"; + aliResult = buildAlipayFailReason(payResult); + returnCommission.setFailMessage(aliResult); + returnCommission.setRemark(aliResult); } } catch (AlipayApiException e) { returnCommission.setStatus("2"); - returnCommission.setFailMessage("支付宝转账失败"); - returnCommission.setRemark(e.getErrMsg()); - aliResult = "支付宝转账失败,请联系管理员!"; + aliResult = buildAlipayExceptionReason(e); + returnCommission.setFailMessage(aliResult); + returnCommission.setRemark(aliResult); } //2.修改提现记录 result = returnCommissionService.save(returnCommission); if (result) { keepRequestLock = true; + if ("2".equals(returnCommission.getStatus())) { + return ResultUtil.error(aliResult); + } return ResultUtil.success("处理完成," + aliResult); } else { return ResultUtil.error("修改提现数据失败," + aliResult); @@ -461,11 +473,46 @@ public class ReturnCommissionController { return ResultUtil.error("支付宝实名名称不能为空!"); } if (StringUtils.isEmpty(returnCommission.getAliAccount())) { - return ResultUtil.error("支付宝手机号不能为空!"); + return ResultUtil.error("支付宝账号不能为空!"); + } + returnCommission.setAliName(returnCommission.getAliName().trim()); + returnCommission.setAliAccount(returnCommission.getAliAccount().trim()); + if (!isValidAliAccount(returnCommission.getAliAccount())) { + return ResultUtil.error("支付宝账号需为手机号或邮箱!"); } return null; } + private boolean isValidAliAccount(String aliAccount) { + final String account = aliAccount == null ? "" : aliAccount.trim(); + return PHONE_PATTERN.matcher(account).matches() || EMAIL_PATTERN.matcher(account).matches(); + } + + private String buildAlipayFailReason(AlipayFundTransUniTransferResponse payResult) { + if (payResult == null) { + return "支付宝转账失败"; + } + return firstNotEmpty(payResult.getSubMsg(), payResult.getMsg(), "支付宝转账失败"); + } + + private String buildAlipayExceptionReason(AlipayApiException e) { + if (e == null) { + return "支付宝转账失败"; + } + return firstNotEmpty(e.getErrMsg(), e.getMessage(), "支付宝转账失败"); + } + + private String firstNotEmpty(String... values) { + if (values != null) { + for (String value : values) { + if (!StringUtils.isEmpty(value)) { + return value; + } + } + } + return "支付宝转账失败"; + } + private String ensureWithdrawId(ReturnCommission returnCommission) { if (StringUtils.isEmpty(returnCommission.getId())) { returnCommission.setId(SnowFlakeUtil.nextId().toString()); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerRelaPriceController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerRelaPriceController.java index b373a67e..76acb318 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerRelaPriceController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerRelaPriceController.java @@ -22,10 +22,13 @@ import cc.hiver.core.entity.Worker; import cc.hiver.core.service.UserService; import cc.hiver.core.service.WorkerService; import cc.hiver.mall.entity.WorkerRelaPrice; +import cc.hiver.mall.ie.service.IeSecurityAuditService; +import cc.hiver.mall.ie.vo.IeAuditResult; import cc.hiver.mall.pojo.vo.WorkerRealPriceVo; import cc.hiver.mall.pojo.vo.WorkerRedisVo; import cc.hiver.mall.service.mybatis.WorkerRelaPriceService; import cc.hiver.mall.utils.WorkerRedisCacheUtil; +import cn.hutool.core.text.CharSequenceUtil; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; @@ -57,6 +60,9 @@ public class WorkerRelaPriceController { @Autowired private UserService userService; + @Autowired + private IeSecurityAuditService auditService; + @RequestMapping(value = "/getByWorkerId", method = RequestMethod.GET) @ApiOperation("根据配送员id获取") public Result getByProductId(@RequestParam(value = "workerId") String workerId) { @@ -81,6 +87,16 @@ public class WorkerRelaPriceController { User user = userService.findById(workerRealPriceVo.getUserId()); Worker worker = workerService.findByUserId(workerRealPriceVo.getUserId()); + if (CharSequenceUtil.isNotBlank(workerRealPriceVo.getWorkerName())) { + Long auditUserId = Long.valueOf(workerRealPriceVo.getUserId()); + IeAuditResult audit = auditService.audit("worker-name", auditUserId, auditUserId, + workerRealPriceVo.getWorkerName(), worker.getRegion()); + if (Boolean.TRUE.equals(audit.getBlocked())) { + throw new RuntimeException("配送员名称未通过审核,请换一个更合适的名称"); + } + workerRealPriceVo.setWorkerName(audit.getFilteredContent()); + } + List workerRelaPriceList = workerRelaPriceService.selectByWorkerId(worker.getWorkerId()); //更新配送员信息 worker.setWorkerName(workerRealPriceVo.getWorkerName()); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/ReturnCommission.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/ReturnCommission.java index 9110fcb5..8a3ae516 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/ReturnCommission.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/ReturnCommission.java @@ -70,7 +70,7 @@ public class ReturnCommission implements Serializable { @ApiModelProperty(value = "支付宝昵称") private String aliName; - @ApiModelProperty(value = "支付宝手机号") + @ApiModelProperty(value = "支付宝账号") private String aliAccount; diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/controller/FishMarketAdminController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/controller/FishMarketAdminController.java index e0118445..f0e1d853 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/controller/FishMarketAdminController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/controller/FishMarketAdminController.java @@ -119,6 +119,13 @@ public class FishMarketAdminController { return ResultUtil.success("处理成功"); } + @RequestMapping(value = "/complaints/unblock", method = RequestMethod.POST) + @ApiOperation("解除举报封禁") + public Result unblockComplaintUser(@RequestBody FishAdminActionDTO dto) { + fishMarketService.unblockComplaintUser(dto, currentUser()); + return ResultUtil.success("解除封禁成功"); + } + @RequestMapping(value = "/users/limits/page", method = RequestMethod.POST) @ApiOperation("用户风控分页") public Result> userLimits(@RequestBody(required = false) FishPageQuery query) { diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/controller/FishMarketController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/controller/FishMarketController.java index 6f81bd9b..34b9714c 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/controller/FishMarketController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/controller/FishMarketController.java @@ -4,6 +4,7 @@ import cc.hiver.core.common.utils.ResultUtil; import cc.hiver.core.common.utils.SecurityUtil; import cc.hiver.core.common.vo.Result; import cc.hiver.core.entity.User; +import cc.hiver.core.service.UserService; import cc.hiver.mall.fishmarket.dto.*; import cc.hiver.mall.fishmarket.entity.*; import cc.hiver.mall.fishmarket.service.FishMarketService; @@ -15,6 +16,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,6 +32,9 @@ public class FishMarketController { @Autowired private SecurityUtil securityUtil; + @Autowired + private UserService userService; + @RequestMapping(value = "/goods/page", method = RequestMethod.POST) @ApiOperation("分页查询二手商品") public Result> pageGoods(@RequestBody(required = false) FishPageQuery query) { @@ -63,14 +68,18 @@ public class FishMarketController { @RequestMapping(value = "/goods", method = RequestMethod.POST) @ApiOperation("发布/编辑出售商品") - public Result publishGoods(@RequestBody FishPublishDTO dto) { - return new ResultUtil().setData(fishMarketService.publishGoods(dto, currentUser())); + public Result> publishGoods(@RequestBody FishPublishDTO dto) { + User user = currentUser(); + FishGoods goods = fishMarketService.publishGoods(dto, user); + return new ResultUtil>().setData(publishResult(goods, user)); } @RequestMapping(value = "/wants", method = RequestMethod.POST) @ApiOperation("发布/编辑求购") - public Result publishWant(@RequestBody FishPublishDTO dto) { - return new ResultUtil().setData(fishMarketService.publishWant(dto, currentUser())); + public Result> publishWant(@RequestBody FishPublishDTO dto) { + User user = currentUser(); + FishWant want = fishMarketService.publishWant(dto, user); + return new ResultUtil>().setData(publishResult(want, user)); } @RequestMapping(value = "/images/audit", method = RequestMethod.POST) @@ -272,4 +281,11 @@ public class FishMarketController { return null; } } + + private Map publishResult(Object data, User user) { + Map result = new HashMap<>(); + result.put("data", data); + result.put("user", userService.findById(user.getId())); + return result; + } } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/service/FishMarketService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/service/FishMarketService.java index b2b4c9ad..b7e067e5 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/service/FishMarketService.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/service/FishMarketService.java @@ -92,6 +92,8 @@ public interface FishMarketService { void handleComplaint(FishAdminActionDTO dto, User admin); + void unblockComplaintUser(FishAdminActionDTO dto, User admin); + FishUserLimit saveUserLimit(FishAdminActionDTO dto, User admin); IPage adminPageComplaints(FishPageQuery query); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/service/impl/FishMarketServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/service/impl/FishMarketServiceImpl.java index fc782c59..6abf109d 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/service/impl/FishMarketServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/service/impl/FishMarketServiceImpl.java @@ -4,6 +4,7 @@ import cc.hiver.core.common.constant.SettingConstant; import cc.hiver.core.common.sms.SmsUtil; import cc.hiver.core.entity.User; import cc.hiver.core.service.UserService; +import cc.hiver.mall.billing.mq.BillingRecordProducer; import cc.hiver.mall.fishmarket.dto.*; import cc.hiver.mall.fishmarket.entity.*; import cc.hiver.mall.fishmarket.mapper.*; @@ -16,12 +17,14 @@ import cc.hiver.mall.ie.service.IeContentAuditLogService; import cc.hiver.mall.ie.service.IeSecurityAuditService; import cc.hiver.mall.ie.vo.IeAliyunModerationResult; import cc.hiver.mall.ie.vo.IeAuditResult; +import cc.hiver.mall.utils.WechatSendMessageUtil; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.mp.bean.template.WxMpTemplateData; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -87,9 +90,14 @@ public class FishMarketServiceImpl implements FishMarketService { private UserService userService; @Autowired(required = false) private SmsUtil smsUtil; + @Autowired(required = false) + private BillingRecordProducer billingRecordProducer; @Autowired private SimpMessagingTemplate messagingTemplate; + @Autowired + private WechatSendMessageUtil wechatSendMessageUtil; + @Autowired private PlatformTransactionManager transactionManager; @@ -103,6 +111,7 @@ public class FishMarketServiceImpl implements FishMarketService { private static final String FISH_COMMENT_UNREAD_KEY_PREFIX = "fishmarket:unread:comment:"; private static final String FISH_SMS_NOTIFIED_KEY_PREFIX = "fishmarket:sms:notified:"; private static final String FISH_SMS_DAILY_SESSION_KEY_PREFIX = "fishmarket:sms:daily:session:"; + private static final String FISH_CHAT_WECHAT_TEMPLATE_ID = "X-lezVQyqm9EUsHCi6ja2cAnjdif5uyA21nhmg0RsuU"; @Override public IPage pageGoods(FishPageQuery query) { @@ -234,8 +243,8 @@ public class FishMarketServiceImpl implements FishMarketService { requireLogin(user); validatePublishUser(user); validatePublishDTO(dto, true); - auditStrictText("fish_goods_title", dto.getId(), user.getId(), dto.getTitle()); - auditStrictText("fish_goods_content", dto.getId(), user.getId(), publishContentText(dto)); + auditStrictText("fish_goods_title", dto.getId(), user.getId(), dto.getTitle(), dto.getRegionId()); + auditStrictText("fish_goods_content", dto.getId(), user.getId(), publishContentText(dto), dto.getRegionId()); FishGoods goods = StringUtils.isNotBlank(dto.getId()) ? goodsMapper.selectById(dto.getId()) : null; if (goods == null) { @@ -288,8 +297,8 @@ public class FishMarketServiceImpl implements FishMarketService { requireLogin(user); validatePublishUser(user); validatePublishDTO(dto, false); - auditStrictText("fish_want_title", dto.getId(), user.getId(), dto.getTitle()); - auditStrictText("fish_want_content", dto.getId(), user.getId(), publishContentText(dto)); + auditStrictText("fish_want_title", dto.getId(), user.getId(), dto.getTitle(), dto.getRegionId()); + auditStrictText("fish_want_content", dto.getId(), user.getId(), publishContentText(dto), dto.getRegionId()); FishWant want = StringUtils.isNotBlank(dto.getId()) ? wantMapper.selectById(dto.getId()) : null; if (want == null) { @@ -327,7 +336,7 @@ public class FishMarketServiceImpl implements FishMarketService { @Override public Map auditReleaseImage(String imageUrl, User user) { requireLogin(user); - auditStrictImages("fish_release_image", user.getId(), Collections.singletonList(imageUrl)); + auditStrictImages("fish_release_image", user.getId(), Collections.singletonList(imageUrl), user.getDepartmentId()); return resultMap("passed", true); } @@ -464,7 +473,7 @@ public class FishMarketServiceImpl implements FishMarketService { throw new RuntimeException("评论内容不能为空"); } FishGoods goods = requireGoods(dto.getGoodsId()); - auditStrictText("fish_comment", null, user.getId(), dto.getContent()); + auditStrictText("fish_comment", null, user.getId(), dto.getContent(), goods.getRegionId()); FishComment parent = StringUtils.isNotBlank(dto.getParentId()) ? commentMapper.selectById(dto.getParentId()) : null; FishComment comment = new FishComment(); comment.setGoodsId(dto.getGoodsId()); @@ -566,8 +575,10 @@ public class FishMarketServiceImpl implements FishMarketService { if (dto == null || StringUtils.isBlank(dto.getComplaintType()) || StringUtils.isBlank(dto.getReason())) { throw new RuntimeException("举报类型和原因不能为空"); } - auditText("fish_complaint", null, user.getId(), dto.getReason()); - auditImages("fish_complaint_image", user.getId(), dto.getImages()); + String regionId = resolveFishRegionId(dto.getGoodsId(), null, dto.getRoomId(), user); + auditText("fish_complaint", null, user.getId(), dto.getReason(), regionId); + auditImages("fish_complaint_image", user.getId(), dto.getImages(), regionId); + boolean commentComplaint = "comment".equals(dto.getComplaintType()) || StringUtils.isNotBlank(dto.getCommentId()); FishComplaint complaint = new FishComplaint(); complaint.setReporterId(user.getId()); complaint.setReporterName(defaultString(user.getNickname(), user.getUsername())); @@ -579,10 +590,23 @@ public class FishMarketServiceImpl implements FishMarketService { complaint.setImages(JSON.toJSONString(limitList(dto.getImages(), 6))); complaint.setStatus(0); complaint.setCreateBy(user.getId()); - if (StringUtils.isNotBlank(dto.getGoodsId())) { - FishGoods goods = goodsMapper.selectById(dto.getGoodsId()); + if (commentComplaint) { + if (StringUtils.isBlank(dto.getCommentId())) { + throw new RuntimeException("举报评论ID不能为空"); + } + FishComment comment = commentMapper.selectById(dto.getCommentId()); + if (comment == null || Integer.valueOf(2).equals(comment.getStatus())) { + throw new RuntimeException("举报评论不存在"); + } + complaint.setGoodsId(comment.getGoodsId()); + complaint.setSellerId(comment.getUserId()); + } + if (StringUtils.isNotBlank(complaint.getGoodsId())) { + FishGoods goods = goodsMapper.selectById(complaint.getGoodsId()); if (goods != null) { - complaint.setSellerId(goods.getUserId()); + if (!commentComplaint) { + complaint.setSellerId(goods.getUserId()); + } goods.setComplaintCount(safeInt(goods.getComplaintCount()) + 1); goodsMapper.updateById(goods); } @@ -680,11 +704,12 @@ public class FishMarketServiceImpl implements FishMarketService { throw new RuntimeException("消息内容不能为空"); } FishChatSession session = requireChatSession(dto.getSessionId(), user); + String regionId = resolveSessionRegionId(session); Integer messageType = dto.getMessageType() == null ? 1 : dto.getMessageType(); if (messageType == 1) { - auditText("fish_chat_message", null, user.getId(), dto.getContent()); + auditText("fish_chat_message", null, user.getId(), dto.getContent(), regionId); } else if (messageType == 2) { - auditImages("fish_chat_message_image", user.getId(), Collections.singletonList(dto.getContent())); + auditImages("fish_chat_message_image", user.getId(), Collections.singletonList(dto.getContent()), regionId); } FishChatMessage message = new FishChatMessage(); message.setSessionId(session.getId()); @@ -711,6 +736,7 @@ public class FishMarketServiceImpl implements FishMarketService { session.setUpdateBy(user.getId()); chatSessionMapper.updateById(session); incrementChatUnread(message.getReceiverId(), session.getId()); + sendWechatChatNotifyAfterCommit(message, session, user); sendUnreadSmsReminderAfterCommit(message.getId()); deliverChatMessage(message); return message; @@ -1012,22 +1038,51 @@ public class FishMarketServiceImpl implements FishMarketService { complaintMapper.updateById(complaint); boolean shouldBlockUser = Boolean.TRUE.equals(dto.getBlockUser()) || dto.getStatus() == null || Integer.valueOf(1).equals(dto.getStatus()); if (shouldBlockUser) { - String sellerUserId = resolveComplaintSellerUserId(complaint); - if (StringUtils.isBlank(sellerUserId)) { - throw new RuntimeException("无法确认被举报商品卖家"); + String targetUserId = resolveComplaintTargetUserId(complaint); + if (StringUtils.isBlank(targetUserId)) { + throw new RuntimeException("无法确认被举报用户"); } FishAdminActionDTO limitDTO = new FishAdminActionDTO(); - limitDTO.setUserId(sellerUserId); + limitDTO.setUserId(targetUserId); limitDTO.setPublishBanned(1); limitDTO.setMuted(1); limitDTO.setPermanentBanned(dto.getPermanentBanned() == null ? 1 : dto.getPermanentBanned()); limitDTO.setReason(dto.getReason()); saveUserLimit(limitDTO, admin); - forceOffShelfComplaintGoods(complaint, dto, admin); + if (isCommentComplaint(complaint)) { + adminDeleteComment(complaint.getCommentId(), admin); + } else { + forceOffShelfComplaintGoods(complaint, dto, admin); + } } logAdmin(admin, "complaint_handle", "complaint", complaint.getId(), dto.getHandleResult()); } + @Override + @Transactional + public void unblockComplaintUser(FishAdminActionDTO dto, User admin) { + if (dto == null || StringUtils.isBlank(dto.getComplaintId())) { + throw new RuntimeException("举报ID不能为空"); + } + FishComplaint complaint = complaintMapper.selectById(dto.getComplaintId()); + if (complaint == null) { + throw new RuntimeException("举报不存在"); + } + String targetUserId = resolveComplaintTargetUserId(complaint); + if (StringUtils.isBlank(targetUserId)) { + throw new RuntimeException("无法确认被举报用户"); + } + String reason = defaultString(dto.getReason(), "后台解除举报封禁"); + FishAdminActionDTO limitDTO = new FishAdminActionDTO(); + limitDTO.setUserId(targetUserId); + limitDTO.setMuted(0); + limitDTO.setPublishBanned(0); + limitDTO.setPermanentBanned(0); + limitDTO.setReason(reason); + saveUserLimit(limitDTO, admin); + logAdmin(admin, "complaint_unblock", "complaint", complaint.getId(), reason); + } + @Override @Transactional public FishUserLimit saveUserLimit(FishAdminActionDTO dto, User admin) { @@ -1130,6 +1185,8 @@ public class FishMarketServiceImpl implements FishMarketService { } try { smsUtil.sendCode(receiver.getMobile(), null, SMS_PRODUCTTALK_TEMPLATE); + recordSmsBilling(resolveSessionRegionId(session), SMS_PRODUCTTALK_TEMPLATE, + receiver.getMobile(), "fish_chat_message", message.getId()); markSmsReminderSent(receiverId, session.getId(), message.getId(), today); int count = today.equals(session.getLastSmsNoticeDate()) ? safeInt(session.getSmsNoticeCount()) : 0; session.setSmsNoticeCount(count + 1); @@ -1164,6 +1221,59 @@ public class FishMarketServiceImpl implements FishMarketService { } } + private String resolveSessionRegionId(FishChatSession session) { + try { + if (session == null) { + return null; + } + if (StringUtils.isNotBlank(session.getGoodsId())) { + FishGoods goods = goodsMapper.selectById(session.getGoodsId()); + return goods == null ? null : goods.getRegionId(); + } + if (StringUtils.isNotBlank(session.getWantId())) { + FishWant want = wantMapper.selectById(session.getWantId()); + return want == null ? null : want.getRegionId(); + } + } catch (Exception e) { + log.warn("校园二手会话校区查询失败,不影响计费记录 sessionId={}", session == null ? null : session.getId(), e); + } + return null; + } + + private String resolveFishRegionId(String goodsId, String wantId, String sessionId, User user) { + try { + if (StringUtils.isNotBlank(goodsId)) { + FishGoods goods = goodsMapper.selectById(goodsId); + if (goods != null && StringUtils.isNotBlank(goods.getRegionId())) { + return goods.getRegionId(); + } + } + if (StringUtils.isNotBlank(wantId)) { + FishWant want = wantMapper.selectById(wantId); + if (want != null && StringUtils.isNotBlank(want.getRegionId())) { + return want.getRegionId(); + } + } + if (StringUtils.isNotBlank(sessionId)) { + FishChatSession session = chatSessionMapper.selectById(sessionId); + String regionId = resolveSessionRegionId(session); + if (StringUtils.isNotBlank(regionId)) { + return regionId; + } + } + } catch (Exception e) { + log.warn("校园二手业务校区查询失败,不影响计费记录 goodsId={}, wantId={}, sessionId={}", + goodsId, wantId, sessionId, e); + } + return user == null ? null : user.getDepartmentId(); + } + + private void recordSmsBilling(String regionId, String smsType, String mobile, String bizType, String bizId) { + if (billingRecordProducer != null) { + billingRecordProducer.recordSms(regionId, smsType, mobile, bizType, bizId); + } + } + private void fillSessionListFields(List sessions, String userId) { if (sessions == null || sessions.isEmpty()) return; Set goodsIds = sessions.stream() @@ -1464,6 +1574,95 @@ public class FishMarketServiceImpl implements FishMarketService { smsReminderProducer.sendUnreadReminder(messageId); } + private void sendWechatChatNotifyAfterCommit(FishChatMessage message, FishChatSession session, User sender) { + if (message == null || session == null || sender == null || StringUtils.isBlank(message.getReceiverId())) return; + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + sendWechatChatNotify(message, session, sender); + } + }); + return; + } + sendWechatChatNotify(message, session, sender); + } + + private void sendWechatChatNotify(FishChatMessage message, FishChatSession session, User sender) { + try { + User receiver = userService.findById(message.getReceiverId()); + if (receiver == null || StringUtils.isBlank(receiver.getOfficialAccountOpenid())) { + return; + } + String itemCode = resolveChatItemCode(session); + String itemTitle = resolveChatItemTitle(session); + List data = new ArrayList<>(); + Date createTime = message.getCreateTime() == null ? new Date() : message.getCreateTime(); + data.add(new WxMpTemplateData("time5", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(createTime))); + data.add(new WxMpTemplateData("thing10", buildContactTemplateValue("半径咨询", itemTitle))); + data.add(new WxMpTemplateData("character_string6", limitCharacterStringValue(itemCode))); + wechatSendMessageUtil.sendWechatTempMessage( + Collections.singletonList(receiver.getOfficialAccountOpenid()), + FISH_CHAT_WECHAT_TEMPLATE_ID, + data, + "pages/index/index" + ); + log.info("校园二手咨询公众号通知已发送,messageId={}, receiverId={}", message.getId(), message.getReceiverId()); + } catch (Exception e) { + log.warn("校园二手咨询公众号通知发送失败,messageId={}, receiverId={}, reason={}", + message.getId(), message.getReceiverId(), e.getMessage()); + } + } + + private String resolveChatItemCode(FishChatSession session) { + if (session == null) { + return "fishmarket"; + } + if (StringUtils.isNotBlank(session.getGoodsId())) { + return session.getGoodsId(); + } + if (StringUtils.isNotBlank(session.getWantId())) { + return session.getWantId(); + } + return defaultString(session.getSessionId(), session.getId()); + } + + private String resolveChatItemTitle(FishChatSession session) { + if (session == null) { + return ""; + } + if (StringUtils.isNotBlank(session.getGoodsId())) { + FishGoods goods = goodsMapper.selectById(session.getGoodsId()); + return goods == null ? "" : StringUtils.defaultString(goods.getTitle()); + } + if (StringUtils.isNotBlank(session.getWantId())) { + FishWant want = wantMapper.selectById(session.getWantId()); + return want == null ? "" : StringUtils.defaultString(want.getTitle()); + } + return ""; + } + + private String buildContactTemplateValue(String contactName, String itemTitle) { + String value = StringUtils.defaultString(contactName); + if (StringUtils.isNotBlank(itemTitle)) { + value = value + "-" + itemTitle; + } + return limitTemplateValue(value, 20); + } + + private String limitTemplateValue(String value, int maxLength) { + String text = StringUtils.defaultString(value); + return text.length() > maxLength ? text.substring(0, maxLength) : text; + } + + private String limitCharacterStringValue(String value) { + String text = StringUtils.defaultString(value).replaceAll("[^A-Za-z0-9_-]", ""); + if (StringUtils.isBlank(text)) { + text = "fishmarket"; + } + return text.length() > 32 ? text.substring(text.length() - 32) : text; + } + private boolean isSmsReminderMessageEligible(FishChatMessage message) { return message != null && StringUtils.isNotBlank(message.getId()) @@ -1550,15 +1749,15 @@ public class FishMarketServiceImpl implements FishMarketService { return contentText.replaceAll("\\s+", "").trim(); } - private void auditText(String bizType, String bizId, String userId, String content) { + private void auditText(String bizType, String bizId, String userId, String content, String regionId) { if (StringUtils.isBlank(content)) return; - IeAuditResult result = securityAuditService.audit(bizType, parseLong(bizId), parseLong(userId), content); + IeAuditResult result = securityAuditService.audit(bizType, parseLong(bizId), parseLong(userId), content, regionId); if (result != null && Boolean.TRUE.equals(result.getBlocked())) { recordFishAuditBlock(bizType, bizId, userId, content, result); throw new RuntimeException(FISH_TEXT_AUDIT_REJECT_MESSAGE); } if (aliyunModerationService != null) { - IeAliyunModerationResult aliyun = aliyunModerationService.checkText(content); + IeAliyunModerationResult aliyun = aliyunModerationService.checkText(content, regionId, bizType, bizId, parseLong(userId)); if (aliyun != null && aliyun.isBlock()) { recordFishAliyunBlock(bizType, bizId, userId, content, aliyun); throw new RuntimeException(FISH_TEXT_AUDIT_REJECT_MESSAGE); @@ -1566,15 +1765,15 @@ public class FishMarketServiceImpl implements FishMarketService { } } - private void auditStrictText(String bizType, String bizId, String userId, String content) { + private void auditStrictText(String bizType, String bizId, String userId, String content, String regionId) { if (StringUtils.isBlank(content)) return; - IeAuditResult result = securityAuditService.audit(bizType, parseLong(bizId), parseLong(userId), content); + IeAuditResult result = securityAuditService.audit(bizType, parseLong(bizId), parseLong(userId), content, regionId); if (result != null && (Boolean.TRUE.equals(result.getBlocked()) || isMediumOrHigh(result.getRiskLevel()))) { recordFishAuditBlock(bizType, bizId, userId, content, result); throw new RuntimeException(FISH_TEXT_AUDIT_REJECT_MESSAGE); } if (aliyunModerationService != null) { - IeAliyunModerationResult aliyun = aliyunModerationService.checkText(content); + IeAliyunModerationResult aliyun = aliyunModerationService.checkText(content, regionId, bizType, bizId, parseLong(userId)); if (aliyun != null && (aliyun.isBlock() || isMediumOrHigh(aliyun.getRiskLevel()))) { recordFishAliyunBlock(bizType, bizId, userId, content, aliyun); throw new RuntimeException(FISH_TEXT_AUDIT_REJECT_MESSAGE); @@ -1582,11 +1781,11 @@ public class FishMarketServiceImpl implements FishMarketService { } } - private void auditImages(String bizType, String userId, List images) { + private void auditImages(String bizType, String userId, List images, String regionId) { if (images == null || images.isEmpty() || aliyunModerationService == null) return; for (String image : images) { if (StringUtils.isBlank(image)) continue; - IeAliyunModerationResult result = aliyunModerationService.checkImage(image); + IeAliyunModerationResult result = aliyunModerationService.checkImage(image, regionId, bizType, null, parseLong(userId)); if (result != null && result.isBlock()) { recordFishAliyunBlock(bizType, null, userId, image, result); throw new RuntimeException(FISH_IMAGE_AUDIT_REJECT_MESSAGE); @@ -1594,11 +1793,11 @@ public class FishMarketServiceImpl implements FishMarketService { } } - private void auditStrictImages(String bizType, String userId, List images) { + private void auditStrictImages(String bizType, String userId, List images, String regionId) { if (images == null || images.isEmpty() || aliyunModerationService == null) return; for (String image : images) { if (StringUtils.isBlank(image)) continue; - IeAliyunModerationResult result = aliyunModerationService.checkImage(image); + IeAliyunModerationResult result = aliyunModerationService.checkImage(image, regionId, bizType, null, parseLong(userId)); if (result != null && (result.isBlock() || isMediumOrHigh(result.getRiskLevel()))) { recordFishAliyunBlock(bizType, null, userId, image, result); throw new RuntimeException(FISH_IMAGE_AUDIT_REJECT_MESSAGE); @@ -1783,6 +1982,23 @@ public class FishMarketServiceImpl implements FishMarketService { return resolveUserId(complaint.getSellerId()); } + private String resolveComplaintTargetUserId(FishComplaint complaint) { + if (complaint == null) return ""; + if (isCommentComplaint(complaint)) { + FishComment comment = StringUtils.isBlank(complaint.getCommentId()) ? null : commentMapper.selectById(complaint.getCommentId()); + if (comment != null && StringUtils.isNotBlank(comment.getUserId())) { + complaint.setSellerId(comment.getUserId()); + return comment.getUserId(); + } + return resolveUserId(complaint.getSellerId()); + } + return resolveComplaintSellerUserId(complaint); + } + + private boolean isCommentComplaint(FishComplaint complaint) { + return complaint != null && ("comment".equals(complaint.getComplaintType()) || StringUtils.isNotBlank(complaint.getCommentId())); + } + private String resolveUserId(String value) { if (StringUtils.isBlank(value)) return ""; if (userService == null) return value; @@ -1794,11 +2010,21 @@ public class FishMarketServiceImpl implements FishMarketService { private void fillComplaintDisplayInfo(List complaints) { if (complaints == null || complaints.isEmpty()) return; - List goodsIds = complaints.stream() + Set commentIds = complaints.stream() + .map(FishComplaint::getCommentId) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toSet()); + Map commentMap = commentIds.isEmpty() ? Collections.emptyMap() : commentMapper.selectBatchIds(commentIds).stream() + .collect(Collectors.toMap(FishComment::getId, comment -> comment, (a, b) -> a)); + + Set goodsIds = complaints.stream() .map(FishComplaint::getGoodsId) .filter(StringUtils::isNotBlank) - .distinct() - .collect(Collectors.toList()); + .collect(Collectors.toSet()); + goodsIds.addAll(commentMap.values().stream() + .map(FishComment::getGoodsId) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toSet())); Map goodsMap = goodsIds.isEmpty() ? Collections.emptyMap() : goodsMapper.selectBatchIds(goodsIds).stream() .collect(Collectors.toMap(FishGoods::getId, goods -> goods, (a, b) -> a)); @@ -1806,6 +2032,10 @@ public class FishMarketServiceImpl implements FishMarketService { .map(FishComplaint::getSellerId) .filter(StringUtils::isNotBlank) .collect(Collectors.toSet()); + sellerIds.addAll(commentMap.values().stream() + .map(FishComment::getUserId) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toSet())); sellerIds.addAll(goodsMap.values().stream() .map(FishGoods::getUserId) .filter(StringUtils::isNotBlank) @@ -1816,12 +2046,20 @@ public class FishMarketServiceImpl implements FishMarketService { .collect(Collectors.toMap(User::getId, user -> user, (a, b) -> a)); for (FishComplaint complaint : complaints) { + FishComment comment = commentMap.get(complaint.getCommentId()); + if (comment != null) { + complaint.setCommentContent(comment.getContent()); + complaint.setGoodsId(StringUtils.defaultIfBlank(complaint.getGoodsId(), comment.getGoodsId())); + if (isCommentComplaint(complaint)) { + complaint.setSellerId(comment.getUserId()); + } + } FishGoods goods = goodsMap.get(complaint.getGoodsId()); if (goods != null) { complaint.setDisplayTitle(goods.getTitle()); complaint.setDisplayCover(goods.getCoverImage()); complaint.setDisplayPrice(goods.getPrice() == null ? null : goods.getPrice().toPlainString()); - if (StringUtils.isBlank(complaint.getSellerId())) { + if (StringUtils.isBlank(complaint.getSellerId()) && !isCommentComplaint(complaint)) { complaint.setSellerId(goods.getUserId()); } } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRecordMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRecordMapper.java index 83b8f19b..93e9112c 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRecordMapper.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRecordMapper.java @@ -2,8 +2,24 @@ package cc.hiver.mall.ie.mapper; import cc.hiver.mall.ie.entity.IeRecord; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; import org.springframework.stereotype.Repository; @Repository public interface IeRecordMapper extends BaseMapper { + @Select("SELECT r.*, (" + + " SELECT COUNT(1) FROM ie_room_message m" + + " WHERE m.room_id = r.room_id" + + " AND m.sender_id = r.target_user_id" + + " AND m.receiver_id = r.user_id" + + " AND m.is_blocked = 0" + + " AND (r.last_read_time IS NULL OR m.create_time > r.last_read_time)" + + ") AS unread_count" + + " FROM ie_record r" + + " WHERE r.user_id = #{userId}" + + " ORDER BY unread_count DESC, COALESCE(r.update_time, r.create_time) DESC, r.create_time DESC") + IPage pageWithUnread(Page page, @Param("userId") Long userId); } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeAliyunModerationService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeAliyunModerationService.java index 03e8d841..4f3bd5f2 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeAliyunModerationService.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeAliyunModerationService.java @@ -8,20 +8,44 @@ import cc.hiver.mall.ie.vo.IeAliyunModerationResult; public interface IeAliyunModerationService { /** 文本同步审核(聊天场景),毫秒级返回结论 */ - IeAliyunModerationResult checkText(String content); + default IeAliyunModerationResult checkText(String content) { + return checkText(content, null, null, null, null); + } + + IeAliyunModerationResult checkText(String content, String regionId, String bizType, String bizId, Long userId); /** 图片同步审核,几百毫秒内返回结论 */ - IeAliyunModerationResult checkImage(String imageUrl); + default IeAliyunModerationResult checkImage(String imageUrl) { + return checkImage(imageUrl, null, null, null, null); + } + + IeAliyunModerationResult checkImage(String imageUrl, String regionId, String bizType, String bizId, Long userId); /** 提交语音文件审核任务,返回 PENDING(taskId) 或 FAILED */ - IeAliyunModerationResult submitVoice(String voiceUrl); + default IeAliyunModerationResult submitVoice(String voiceUrl) { + return submitVoice(voiceUrl, null, null, null, null); + } + + IeAliyunModerationResult submitVoice(String voiceUrl, String regionId, String bizType, String bizId, Long userId); /** 提交视频文件审核任务(云端自动抽帧+音轨检测),返回 PENDING(taskId) 或 FAILED */ - IeAliyunModerationResult submitVideo(String videoUrl); + default IeAliyunModerationResult submitVideo(String videoUrl) { + return submitVideo(videoUrl, null, null, null, null); + } + + IeAliyunModerationResult submitVideo(String videoUrl, String regionId, String bizType, String bizId, Long userId); /** 查询语音审核任务结果 */ - IeAliyunModerationResult queryVoiceResult(String taskId); + default IeAliyunModerationResult queryVoiceResult(String taskId) { + return queryVoiceResult(taskId, null, null, null, null); + } + + IeAliyunModerationResult queryVoiceResult(String taskId, String regionId, String bizType, String bizId, Long userId); /** 查询视频审核任务结果 */ - IeAliyunModerationResult queryVideoResult(String taskId); + default IeAliyunModerationResult queryVideoResult(String taskId) { + return queryVideoResult(taskId, null, null, null, null); + } + + IeAliyunModerationResult queryVideoResult(String taskId, String regionId, String bizType, String bizId, Long userId); } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeSecurityAuditService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeSecurityAuditService.java index a03f3905..6700db8e 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeSecurityAuditService.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeSecurityAuditService.java @@ -3,5 +3,9 @@ package cc.hiver.mall.ie.service; import cc.hiver.mall.ie.vo.IeAuditResult; public interface IeSecurityAuditService { - IeAuditResult audit(String bizType, Long bizId, Long userId, String content); + default IeAuditResult audit(String bizType, Long bizId, Long userId, String content) { + return audit(bizType, bizId, userId, content, null); + } + + IeAuditResult audit(String bizType, Long bizId, Long userId, String content, String regionId); } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeAliyunModerationServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeAliyunModerationServiceImpl.java index f834aa72..f1c878a6 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeAliyunModerationServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeAliyunModerationServiceImpl.java @@ -1,5 +1,6 @@ package cc.hiver.mall.ie.service.impl; +import cc.hiver.mall.billing.mq.BillingRecordProducer; import cc.hiver.mall.ie.service.IeAliyunModerationService; import cc.hiver.mall.ie.vo.IeAliyunModerationResult; import cn.hutool.core.text.CharSequenceUtil; @@ -9,6 +10,7 @@ import com.aliyun.green20220302.models.*; import com.aliyun.teaopenapi.models.Config; import com.aliyun.teautil.models.RuntimeOptions; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -50,23 +52,26 @@ public class IeAliyunModerationServiceImpl implements IeAliyunModerationService @Value("${ie.aliyun.green.block-level:high}") private String blockLevel; + @Autowired(required = false) + private BillingRecordProducer billingRecordProducer; + private volatile Client client; @Override - public IeAliyunModerationResult checkText(String content) { + public IeAliyunModerationResult checkText(String content, String regionId, String bizType, String bizId, Long userId) { if (CharSequenceUtil.isBlank(content)) { return IeAliyunModerationResult.pass(null, "empty content"); } - IeAliyunModerationResult result = doCheckText(content); + IeAliyunModerationResult result = doCheckText(content, regionId, bizType, bizId, userId); if (result.isFailed()) { // 网络抖动/限流等瞬时失败重试一次,避免正常内容被误判为审核失败 log.warn("阿里云文本审核首次失败,重试一次 detail={}", result.getDetail()); - result = doCheckText(content); + result = doCheckText(content, regionId, bizType, bizId, userId); } return result; } - private IeAliyunModerationResult doCheckText(String content) { + private IeAliyunModerationResult doCheckText(String content, String regionId, String bizType, String bizId, Long userId) { try { JSONObject parameters = new JSONObject(); parameters.set("content", content); @@ -75,42 +80,47 @@ public class IeAliyunModerationServiceImpl implements IeAliyunModerationService request.setServiceParameters(parameters.toString()); TextModerationResponse response = getClient().textModerationWithOptions(request, runtime()); if (response == null || response.getBody() == null) { - return IeAliyunModerationResult.failed("empty text moderation response"); + return recordContentAudit(regionId, "text", bizType, bizId, userId, + IeAliyunModerationResult.failed("empty text moderation response")); } TextModerationResponseBody body = response.getBody(); if (body.getCode() == null || body.getCode() != CODE_OK || body.getData() == null) { - return IeAliyunModerationResult.failed("text moderation code=" + body.getCode() + ", msg=" + body.getMessage()); + return recordContentAudit(regionId, "text", bizType, bizId, userId, + IeAliyunModerationResult.failed("text moderation code=" + body.getCode() + ", msg=" + body.getMessage())); } // 命中风险时 labels 非空(如 porn,abuse),无风险为空串 String labels = body.getData().getLabels(); if (CharSequenceUtil.isBlank(labels)) { - return IeAliyunModerationResult.pass(null, "text pass"); + return recordContentAudit(regionId, "text", bizType, bizId, userId, + IeAliyunModerationResult.pass(null, "text pass")); } // 文本的风险等级在 reason JSON 里({"riskLevel":"high",...}),按等级阈值统一判定, // 不再"命中任何标签就拦"(轻度辱骂等 low/medium 误伤太多) String riskLevel = parseTextRiskLevel(body.getData().getReason()); - return toFinalResult(riskLevel, "text labels=" + labels + ", riskLevel=" + riskLevel - + ", reason=" + body.getData().getReason()); + return recordContentAudit(regionId, "text", bizType, bizId, userId, + toFinalResult(riskLevel, "text labels=" + labels + ", riskLevel=" + riskLevel + + ", reason=" + body.getData().getReason())); } catch (Exception e) { log.warn("阿里云文本审核调用失败", e); - return IeAliyunModerationResult.failed("text moderation error: " + e.getMessage()); + return recordContentAudit(regionId, "text", bizType, bizId, userId, + IeAliyunModerationResult.failed("text moderation error: " + e.getMessage())); } } @Override - public IeAliyunModerationResult checkImage(String imageUrl) { + public IeAliyunModerationResult checkImage(String imageUrl, String regionId, String bizType, String bizId, Long userId) { if (CharSequenceUtil.isBlank(imageUrl)) { return IeAliyunModerationResult.failed("image url missing"); } - IeAliyunModerationResult result = doCheckImage(imageUrl); + IeAliyunModerationResult result = doCheckImage(imageUrl, regionId, bizType, bizId, userId); if (result.isFailed()) { log.warn("阿里云图片审核首次失败,重试一次 url={}, detail={}", imageUrl, result.getDetail()); - result = doCheckImage(imageUrl); + result = doCheckImage(imageUrl, regionId, bizType, bizId, userId); } return result; } - private IeAliyunModerationResult doCheckImage(String imageUrl) { + private IeAliyunModerationResult doCheckImage(String imageUrl, String regionId, String bizType, String bizId, Long userId) { try { JSONObject parameters = new JSONObject(); parameters.set("imageUrl", imageUrl); @@ -120,11 +130,13 @@ public class IeAliyunModerationServiceImpl implements IeAliyunModerationService request.setServiceParameters(parameters.toString()); ImageModerationResponse response = getClient().imageModerationWithOptions(request, runtime()); if (response == null || response.getBody() == null) { - return IeAliyunModerationResult.failed("empty image moderation response"); + return recordContentAudit(regionId, "image", bizType, bizId, userId, + IeAliyunModerationResult.failed("empty image moderation response")); } ImageModerationResponseBody body = response.getBody(); if (body.getCode() == null || body.getCode() != CODE_OK || body.getData() == null) { - return IeAliyunModerationResult.failed("image moderation code=" + body.getCode() + ", msg=" + body.getMsg()); + return recordContentAudit(regionId, "image", bizType, bizId, userId, + IeAliyunModerationResult.failed("image moderation code=" + body.getCode() + ", msg=" + body.getMsg())); } String riskLevel = body.getData().getRiskLevel(); // 带上命中标签和置信度,方便在日志里直接定位误判原因 @@ -139,15 +151,17 @@ public class IeAliyunModerationServiceImpl implements IeAliyunModerationService } }); } - return toFinalResult(riskLevel, "image riskLevel=" + riskLevel + ", labels=" + labels + ", url=" + imageUrl); + return recordContentAudit(regionId, "image", bizType, bizId, userId, + toFinalResult(riskLevel, "image riskLevel=" + riskLevel + ", labels=" + labels + ", url=" + imageUrl)); } catch (Exception e) { log.warn("阿里云图片审核调用失败 url={}", imageUrl, e); - return IeAliyunModerationResult.failed("image moderation error: " + e.getMessage()); + return recordContentAudit(regionId, "image", bizType, bizId, userId, + IeAliyunModerationResult.failed("image moderation error: " + e.getMessage())); } } @Override - public IeAliyunModerationResult submitVoice(String voiceUrl) { + public IeAliyunModerationResult submitVoice(String voiceUrl, String regionId, String bizType, String bizId, Long userId) { if (CharSequenceUtil.isBlank(voiceUrl)) { return IeAliyunModerationResult.failed("voice url missing"); } @@ -162,18 +176,21 @@ public class IeAliyunModerationServiceImpl implements IeAliyunModerationService if (response == null || response.getBody() == null || response.getBody().getCode() == null || response.getBody().getCode() != CODE_OK || response.getBody().getData() == null || CharSequenceUtil.isBlank(response.getBody().getData().getTaskId())) { - return IeAliyunModerationResult.failed("voice submit failed: " - + (response == null || response.getBody() == null ? "empty response" : response.getBody().getMessage())); + return recordContentAudit(regionId, "voice_submit", bizType, bizId, userId, + IeAliyunModerationResult.failed("voice submit failed: " + + (response == null || response.getBody() == null ? "empty response" : response.getBody().getMessage()))); } - return IeAliyunModerationResult.pending(response.getBody().getData().getTaskId()); + return recordContentAudit(regionId, "voice_submit", bizType, bizId, userId, + IeAliyunModerationResult.pending(response.getBody().getData().getTaskId())); } catch (Exception e) { log.warn("阿里云语音审核提交失败 url={}", voiceUrl, e); - return IeAliyunModerationResult.failed("voice submit error: " + e.getMessage()); + return recordContentAudit(regionId, "voice_submit", bizType, bizId, userId, + IeAliyunModerationResult.failed("voice submit error: " + e.getMessage())); } } @Override - public IeAliyunModerationResult submitVideo(String videoUrl) { + public IeAliyunModerationResult submitVideo(String videoUrl, String regionId, String bizType, String bizId, Long userId) { if (CharSequenceUtil.isBlank(videoUrl)) { return IeAliyunModerationResult.failed("video url missing"); } @@ -188,18 +205,21 @@ public class IeAliyunModerationServiceImpl implements IeAliyunModerationService if (response == null || response.getBody() == null || response.getBody().getCode() == null || response.getBody().getCode() != CODE_OK || response.getBody().getData() == null || CharSequenceUtil.isBlank(response.getBody().getData().getTaskId())) { - return IeAliyunModerationResult.failed("video submit failed: " - + (response == null || response.getBody() == null ? "empty response" : response.getBody().getMessage())); + return recordContentAudit(regionId, "video_submit", bizType, bizId, userId, + IeAliyunModerationResult.failed("video submit failed: " + + (response == null || response.getBody() == null ? "empty response" : response.getBody().getMessage()))); } - return IeAliyunModerationResult.pending(response.getBody().getData().getTaskId()); + return recordContentAudit(regionId, "video_submit", bizType, bizId, userId, + IeAliyunModerationResult.pending(response.getBody().getData().getTaskId())); } catch (Exception e) { log.warn("阿里云视频审核提交失败 url={}", videoUrl, e); - return IeAliyunModerationResult.failed("video submit error: " + e.getMessage()); + return recordContentAudit(regionId, "video_submit", bizType, bizId, userId, + IeAliyunModerationResult.failed("video submit error: " + e.getMessage())); } } @Override - public IeAliyunModerationResult queryVoiceResult(String taskId) { + public IeAliyunModerationResult queryVoiceResult(String taskId, String regionId, String bizType, String bizId, Long userId) { if (CharSequenceUtil.isBlank(taskId)) { return IeAliyunModerationResult.failed("voice taskId missing"); } @@ -211,25 +231,30 @@ public class IeAliyunModerationServiceImpl implements IeAliyunModerationService request.setServiceParameters(parameters.toString()); VoiceModerationResultResponse response = getClient().voiceModerationResultWithOptions(request, runtime()); if (response == null || response.getBody() == null || response.getBody().getCode() == null) { - return IeAliyunModerationResult.failed("empty voice result response"); + return recordContentAudit(regionId, "voice_query", bizType, bizId, userId, + IeAliyunModerationResult.failed("empty voice result response")); } VoiceModerationResultResponseBody body = response.getBody(); if (body.getCode() == CODE_PROCESSING) { - return IeAliyunModerationResult.pending(taskId); + return recordContentAudit(regionId, "voice_query", bizType, bizId, userId, + IeAliyunModerationResult.pending(taskId)); } if (body.getCode() != CODE_OK || body.getData() == null) { - return IeAliyunModerationResult.failed("voice result code=" + body.getCode() + ", msg=" + body.getMessage()); + return recordContentAudit(regionId, "voice_query", bizType, bizId, userId, + IeAliyunModerationResult.failed("voice result code=" + body.getCode() + ", msg=" + body.getMessage())); } String riskLevel = body.getData().getRiskLevel(); - return toFinalResult(riskLevel, "voice riskLevel=" + riskLevel + ", taskId=" + taskId); + return recordContentAudit(regionId, "voice_query", bizType, bizId, userId, + toFinalResult(riskLevel, "voice riskLevel=" + riskLevel + ", taskId=" + taskId)); } catch (Exception e) { log.warn("阿里云语音审核结果查询失败 taskId={}", taskId, e); - return IeAliyunModerationResult.failed("voice result error: " + e.getMessage()); + return recordContentAudit(regionId, "voice_query", bizType, bizId, userId, + IeAliyunModerationResult.failed("voice result error: " + e.getMessage())); } } @Override - public IeAliyunModerationResult queryVideoResult(String taskId) { + public IeAliyunModerationResult queryVideoResult(String taskId, String regionId, String bizType, String bizId, Long userId) { if (CharSequenceUtil.isBlank(taskId)) { return IeAliyunModerationResult.failed("video taskId missing"); } @@ -241,20 +266,25 @@ public class IeAliyunModerationServiceImpl implements IeAliyunModerationService request.setServiceParameters(parameters.toString()); VideoModerationResultResponse response = getClient().videoModerationResultWithOptions(request, runtime()); if (response == null || response.getBody() == null || response.getBody().getCode() == null) { - return IeAliyunModerationResult.failed("empty video result response"); + return recordContentAudit(regionId, "video_query", bizType, bizId, userId, + IeAliyunModerationResult.failed("empty video result response")); } VideoModerationResultResponseBody body = response.getBody(); if (body.getCode() == CODE_PROCESSING) { - return IeAliyunModerationResult.pending(taskId); + return recordContentAudit(regionId, "video_query", bizType, bizId, userId, + IeAliyunModerationResult.pending(taskId)); } if (body.getCode() != CODE_OK || body.getData() == null) { - return IeAliyunModerationResult.failed("video result code=" + body.getCode() + ", msg=" + body.getMessage()); + return recordContentAudit(regionId, "video_query", bizType, bizId, userId, + IeAliyunModerationResult.failed("video result code=" + body.getCode() + ", msg=" + body.getMessage())); } String riskLevel = body.getData().getRiskLevel(); - return toFinalResult(riskLevel, "video riskLevel=" + riskLevel + ", taskId=" + taskId); + return recordContentAudit(regionId, "video_query", bizType, bizId, userId, + toFinalResult(riskLevel, "video riskLevel=" + riskLevel + ", taskId=" + taskId)); } catch (Exception e) { log.warn("阿里云视频审核结果查询失败 taskId={}", taskId, e); - return IeAliyunModerationResult.failed("video result error: " + e.getMessage()); + return recordContentAudit(regionId, "video_query", bizType, bizId, userId, + IeAliyunModerationResult.failed("video result error: " + e.getMessage())); } } @@ -302,6 +332,31 @@ public class IeAliyunModerationServiceImpl implements IeAliyunModerationService } } + private IeAliyunModerationResult recordContentAudit(String regionId, String usageType, String bizType, + String bizId, Long userId, IeAliyunModerationResult result) { + if (billingRecordProducer != null && result != null) { + billingRecordProducer.recordContentAudit(regionId, usageType, bizType, bizId, + userId == null ? null : String.valueOf(userId), resultStatus(result), result.getTaskId(), result.getDetail()); + } + return result; + } + + private String resultStatus(IeAliyunModerationResult result) { + if (result.isBlock()) { + return "block"; + } + if (result.isPass()) { + return "pass"; + } + if (result.isPending()) { + return "pending"; + } + if (result.isFailed()) { + return "failed"; + } + return "unknown"; + } + private RuntimeOptions runtime() { RuntimeOptions runtime = new RuntimeOptions(); // 小带宽/突发性能实例上网卡瞬时拥塞时 TCP 握手会变慢,超时给宽一些,配合 MQ 延迟重试兜底 diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java index b9ab2a86..868d6cf9 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java @@ -130,14 +130,16 @@ public class IeChatServiceImpl implements IeChatService { Integer messageType = dto.getMessageType() == null ? 1 : dto.getMessageType(); assertMediaCapacity(senderId, room.getId(), messageType); validateMediaPayload(messageType, dto); - IeAuditResult audit = auditMessage(room.getId(), senderId, messageType, dto); + String senderRegionId = profileRegionId(senderId); + IeAuditResult audit = auditMessage(room.getId(), senderId, messageType, dto, senderRegionId); // 图片走阿里云同步审核,毫秒级出结论,无需异步等待 boolean imageBlocked = false; IeAliyunModerationResult imageResult = null; if (messageType == 2 && !Boolean.TRUE.equals(audit.getBlocked())) { imageResult = aliyunModerationService.checkImage( - firstNotBlank(dto.getMediaCheckUrl(), dto.getContent())); + firstNotBlank(dto.getMediaCheckUrl(), dto.getContent()), senderRegionId, + "message-image", String.valueOf(room.getId()), senderId); if (imageResult.isBlock()) { imageBlocked = true; } else if (!imageResult.isPass()) { @@ -346,7 +348,7 @@ public class IeChatServiceImpl implements IeChatService { redisService.deleteCachedRoom(room.getId()); } - private IeAuditResult auditMessage(Long roomId, Long senderId, Integer messageType, IeRoomMessageDTO dto) { + private IeAuditResult auditMessage(Long roomId, Long senderId, Integer messageType, IeRoomMessageDTO dto, String regionId) { String content = dto.getContent(); if (messageType != null && (messageType == 2 || messageType == 4 || messageType == 5)) { IeAuditResult result = new IeAuditResult(); @@ -357,7 +359,7 @@ public class IeChatServiceImpl implements IeChatService { result.setBlocked(false); return result; } - return auditService.audit("message", roomId, senderId, content); + return auditService.audit("message", roomId, senderId, content, regionId); } private Integer safeMediaDuration(Integer mediaDuration) { @@ -375,7 +377,7 @@ public class IeChatServiceImpl implements IeChatService { // 轻互动文案会实时推给对方,带文字时同样要过审 String eventText = dto.getEventText(); if (CharSequenceUtil.isNotBlank(eventText)) { - IeAuditResult audit = auditService.audit("presence", room.getId(), senderId, eventText); + IeAuditResult audit = auditService.audit("presence", room.getId(), senderId, eventText, profileRegionId(senderId)); if (Boolean.TRUE.equals(audit.getBlocked())) { throw new RuntimeException("这条互动内容不太合适,换一个吧"); } @@ -617,7 +619,8 @@ public class IeChatServiceImpl implements IeChatService { // 视频封面是用户上传的图片,先同步审一道,违规直接拦截省一次视频任务 if (type != null && type == 5 && message.getMediaCheckType() != null && message.getMediaCheckType() == 2 && CharSequenceUtil.isNotBlank(message.getMediaCheckUrl())) { - IeAliyunModerationResult coverResult = aliyunModerationService.checkImage(message.getMediaCheckUrl()); + IeAliyunModerationResult coverResult = aliyunModerationService.checkImage(message.getMediaCheckUrl(), + profileRegionId(message.getSenderId()), "message-video-cover", String.valueOf(message.getId()), message.getSenderId()); if (coverResult.isBlock()) { contentAuditLogService.recordAliyunBlock("message-video-cover", message.getId(), message.getSenderId(), message.getMediaCheckUrl(), coverResult); @@ -627,9 +630,11 @@ public class IeChatServiceImpl implements IeChatService { } IeAliyunModerationResult submitResult; if (type != null && type == 5) { - submitResult = aliyunModerationService.submitVideo(message.getRawContent()); + submitResult = aliyunModerationService.submitVideo(message.getRawContent(), profileRegionId(message.getSenderId()), + "message-video", String.valueOf(message.getId()), message.getSenderId()); } else { - submitResult = aliyunModerationService.submitVoice(firstNotBlank(message.getMediaCheckUrl(), message.getRawContent())); + submitResult = aliyunModerationService.submitVoice(firstNotBlank(message.getMediaCheckUrl(), message.getRawContent()), + profileRegionId(message.getSenderId()), "message-voice", String.valueOf(message.getId()), message.getSenderId()); } if (!submitResult.isPending() || CharSequenceUtil.isBlank(submitResult.getTaskId())) { // 提交失败多为网络抖动/限流等瞬时问题,延迟重试而不是立刻判失败; @@ -657,9 +662,10 @@ public class IeChatServiceImpl implements IeChatService { return; } Integer type = message.getMessageType(); + String regionId = profileRegionId(message.getSenderId()); IeAliyunModerationResult result = type != null && type == 5 - ? aliyunModerationService.queryVideoResult(taskId) - : aliyunModerationService.queryVoiceResult(taskId); + ? aliyunModerationService.queryVideoResult(taskId, regionId, "message-video", String.valueOf(message.getId()), message.getSenderId()) + : aliyunModerationService.queryVoiceResult(taskId, regionId, "message-voice", String.valueOf(message.getId()), message.getSenderId()); if (result.isPending()) { // 还在审,继续轮询;10 分钟超时兜底会把卡死的任务标记失败 chatEventProducer.sendMediaAuditPoll(messageId, MEDIA_AUDIT_POLL_MILLIS); @@ -785,6 +791,16 @@ public class IeChatServiceImpl implements IeChatService { .last("limit 1")); } + private String profileRegionId(Long userId) { + try { + IeUserProfile profile = findProfileByUserId(userId); + return profile == null ? null : profile.getRegionId(); + } catch (Exception e) { + log.warn("查询i/e用户校区失败,不影响阿里云计费记录 userId={}", userId, e); + return null; + } + } + private void createChatRecord(IeRoom room, Long userId, Long targetUserId, IeUserProfile targetProfile) { IeRecord exists = recordMapper.selectOne(new LambdaQueryWrapper() .eq(IeRecord::getRoomId, room.getId()) @@ -913,47 +929,15 @@ public class IeChatServiceImpl implements IeChatService { public IPage pageRecords(Long userId, Integer pageNumber, Integer pageSize) { long current = safePageNumber(pageNumber); long size = safePageSize(pageSize); - Page result = new Page<>(current, size); - List records = recordMapper.selectList(new LambdaQueryWrapper() - .eq(IeRecord::getUserId, userId) - .orderByDesc(IeRecord::getUpdateTime) - .orderByDesc(IeRecord::getCreateTime)); + IPage result = recordMapper.pageWithUnread(new Page<>(current, size), userId); + List records = result.getRecords(); + fillLatestTargetProfiles(records); for (IeRecord record : records) { - record.setUnreadCount(unreadMessageCount(userId, record.getRoomId(), record.getTargetUserId())); - fillLatestTargetProfile(record); fillRoomStreak(record); } - records.sort((left, right) -> { - int unreadCompare = Integer.compare(right.getUnreadCount() == null ? 0 : right.getUnreadCount(), - left.getUnreadCount() == null ? 0 : left.getUnreadCount()); - if (unreadCompare != 0) { - return unreadCompare; - } - return compareRecordTimeDesc(left, right); - }); - int fromIndex = (int) Math.min((current - 1) * size, (long) records.size()); - int toIndex = (int) Math.min(fromIndex + size, (long) records.size()); - result.setRecords(records.subList(fromIndex, toIndex)); - result.setTotal(records.size()); - result.setPages((records.size() + size - 1L) / size); return result; } - private int compareRecordTimeDesc(IeRecord left, IeRecord right) { - Date leftTime = left.getUpdateTime() == null ? left.getCreateTime() : left.getUpdateTime(); - Date rightTime = right.getUpdateTime() == null ? right.getCreateTime() : right.getUpdateTime(); - if (leftTime == null && rightTime == null) { - return 0; - } - if (leftTime == null) { - return 1; - } - if (rightTime == null) { - return -1; - } - return rightTime.compareTo(leftTime); - } - private void fillRoomStreak(IeRecord record) { if (record == null || record.getRoomId() == null) { return; @@ -984,12 +968,37 @@ public class IeChatServiceImpl implements IeChatService { // 否则其它会话尚未拉取的离线消息会永久丢失。 } - private void fillLatestTargetProfile(IeRecord record) { - if (record == null || record.getTargetUserId() == null) { + private void fillLatestTargetProfiles(List records) { + if (records == null || records.isEmpty()) { return; } - IeUserProfile profile = findProfileByUserId(record.getTargetUserId()); - if (profile == null || !isCompletedProfile(profile)) { + Set targetUserIds = new HashSet<>(); + for (IeRecord record : records) { + if (record != null && record.getTargetUserId() != null) { + targetUserIds.add(record.getTargetUserId()); + } + } + if (targetUserIds.isEmpty()) { + return; + } + List profiles = userProfileMapper.selectList(new LambdaQueryWrapper() + .in(IeUserProfile::getUserId, targetUserIds)); + Map profileMap = new HashMap<>(); + for (IeUserProfile profile : profiles) { + if (profile != null && profile.getUserId() != null && isCompletedProfile(profile)) { + profileMap.putIfAbsent(profile.getUserId(), profile); + } + } + for (IeRecord record : records) { + if (record == null || record.getTargetUserId() == null) { + continue; + } + fillTargetProfile(record, profileMap.get(record.getTargetUserId())); + } + } + + private void fillTargetProfile(IeRecord record, IeUserProfile profile) { + if (record == null || profile == null || !isCompletedProfile(profile)) { return; } record.setAnonymousName(profile.getAnonymousName()); @@ -1003,27 +1012,6 @@ public class IeChatServiceImpl implements IeChatService { return profile != null && profile.getProfileCompleted() != null && profile.getProfileCompleted() == 1; } - private int unreadMessageCount(Long userId, Long roomId, Long targetUserId) { - if (roomId == null || targetUserId == null) { - return 0; - } - Long count = roomMessageMapper.selectCount(new LambdaQueryWrapper() - .eq(IeRoomMessage::getRoomId, roomId) - .eq(IeRoomMessage::getSenderId, targetUserId) - .eq(IeRoomMessage::getReceiverId, userId) - .eq(IeRoomMessage::getIsBlocked, 0) - .gt(recordLastReadTime(userId, roomId) != null, IeRoomMessage::getCreateTime, recordLastReadTime(userId, roomId))); - return count == null ? 0 : count.intValue(); - } - - private Date recordLastReadTime(Long userId, Long roomId) { - IeRecord record = recordMapper.selectOne(new LambdaQueryWrapper() - .eq(IeRecord::getUserId, userId) - .eq(IeRecord::getRoomId, roomId) - .last("limit 1")); - return record == null ? null : record.getLastReadTime(); - } - @Override public IPage pageReports(Long userId, Integer pageNumber, Integer pageSize) { Page page = new Page<>(safePageNumber(pageNumber), safePageSize(pageSize)); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeDailyQuestionServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeDailyQuestionServiceImpl.java index 66096b8d..ece7dbed 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeDailyQuestionServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeDailyQuestionServiceImpl.java @@ -93,13 +93,14 @@ public class IeDailyQuestionServiceImpl implements IeDailyQuestionService { if (question == null) { throw new RuntimeException("今天还没有题目,明天再来吧"); } - IeAuditResult audit = auditService.audit("daily-answer", question.getId(), userId, trimmed); - if (Boolean.TRUE.equals(audit.getBlocked())) { - throw new RuntimeException("回答内容包含不适合展示的表达,换一种说法吧"); - } IeUserProfile profile = userProfileMapper.selectOne(new LambdaQueryWrapper() .eq(IeUserProfile::getUserId, userId) .last("limit 1")); + IeAuditResult audit = auditService.audit("daily-answer", question.getId(), userId, trimmed, + profile == null ? null : profile.getRegionId()); + if (Boolean.TRUE.equals(audit.getBlocked())) { + throw new RuntimeException("回答内容包含不适合展示的表达,换一种说法吧"); + } IeDailyAnswer answer = new IeDailyAnswer(); answer.setQuestionId(question.getId()); answer.setUserId(userId); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java index f3779c98..dbd701fb 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java @@ -161,10 +161,11 @@ public class IeMatchServiceImpl implements IeMatchService { if (dto == null) { dto = new IeProfileDTO(); } + String auditRegionId = defaultText(dto.getRegionId(), profile.getRegionId()); // 昵称/一句话介绍单独审(取过滤后的文案),头像字、兴趣标签、地区名合并成一次审核(只判拦截) - String anonymousName = auditProfileText("profile-name", userId, defaultText(dto.getAnonymousName(), profile.getAnonymousName())); - String intro = auditProfileText("profile-intro", userId, defaultText(dto.getIntro(), "")); - String companionIntent = auditCompanionIntent(userId, defaultText(dto.getCompanionIntent(), profile.getCompanionIntent())); + String anonymousName = auditProfileText("profile-name", userId, defaultText(dto.getAnonymousName(), profile.getAnonymousName()), auditRegionId); + String intro = auditProfileText("profile-intro", userId, defaultText(dto.getIntro(), ""), auditRegionId); + String companionIntent = auditCompanionIntent(userId, defaultText(dto.getCompanionIntent(), profile.getCompanionIntent()), auditRegionId); String avatarText = defaultText(dto.getAvatarText(), "我"); StringBuilder extraText = new StringBuilder(avatarText); if (dto.getInterestTags() != null) { @@ -177,11 +178,11 @@ public class IeMatchServiceImpl implements IeMatchService { if (dto.getRegionName() != null && !dto.getRegionName().trim().isEmpty()) { extraText.append(' ').append(dto.getRegionName().trim()); } - auditProfileText("profile-extra", userId, extraText.toString()); + auditProfileText("profile-extra", userId, extraText.toString(), auditRegionId); profile.setAnonymousName(anonymousName); profile.setAvatarText(avatarText); if (dto.getAvatarUrl() != null && !dto.getAvatarUrl().trim().isEmpty()) { - auditProfileImage(userId, "profile-avatar", dto.getAvatarUrl().trim(), profile.getAvatarUrl()); + auditProfileImage(userId, "profile-avatar", dto.getAvatarUrl().trim(), profile.getAvatarUrl(), auditRegionId); profile.setAvatarUrl(dto.getAvatarUrl()); } profile.setGender(normalizeGender(dto.getGender(), "unknown")); @@ -193,7 +194,7 @@ public class IeMatchServiceImpl implements IeMatchService { String existingImages = profile.getPersonaImages() == null ? "" : profile.getPersonaImages(); for (String image : dto.getPersonaImages()) { if (image != null && !image.trim().isEmpty() && !existingImages.contains(image.trim())) { - auditProfileImage(userId, "profile-persona-image", image.trim(), null); + auditProfileImage(userId, "profile-persona-image", image.trim(), null, auditRegionId); } } } @@ -246,7 +247,7 @@ public class IeMatchServiceImpl implements IeMatchService { String statusText = dto.getStatusText(); if (statusText != null && !statusText.trim().isEmpty() && (status == null || !statusText.equals(status.getStatusText()))) { - IeAuditResult audit = auditService.audit("status-text", userId, userId, statusText); + IeAuditResult audit = auditService.audit("status-text", userId, userId, statusText, dto.getRegionId()); if (Boolean.TRUE.equals(audit.getBlocked())) { throw new RuntimeException("这条状态不太合适展示,换个说法吧"); } @@ -1082,7 +1083,11 @@ public class IeMatchServiceImpl implements IeMatchService { } private String auditProfileText(String bizType, Long userId, String content) { - IeAuditResult result = auditService.audit(bizType, userId, userId, content); + return auditProfileText(bizType, userId, content, safeProfileRegionId(userId)); + } + + private String auditProfileText(String bizType, Long userId, String content, String regionId) { + IeAuditResult result = auditService.audit(bizType, userId, userId, content, regionId); if (Boolean.TRUE.equals(result.getBlocked())) { throw new RuntimeException("资料内容包含不适合展示的表达,请换一种说法"); } @@ -1090,6 +1095,10 @@ public class IeMatchServiceImpl implements IeMatchService { } private String auditCompanionIntent(Long userId, String content) { + return auditCompanionIntent(userId, content, safeProfileRegionId(userId)); + } + + private String auditCompanionIntent(Long userId, String content, String regionId) { if (!hasText(content)) { return ""; } @@ -1097,7 +1106,7 @@ public class IeMatchServiceImpl implements IeMatchService { if (trimmed.length() > 24) { throw new RuntimeException("找搭子内容不能超过 24 个字"); } - return auditProfileText("profile-companion-intent", userId, trimmed); + return auditProfileText("profile-companion-intent", userId, trimmed, regionId); } private String auditAndSaveMatchCompanionIntent(Long userId, IeUserProfile profile, String content) { @@ -1115,10 +1124,23 @@ public class IeMatchServiceImpl implements IeMatchService { } private void auditProfileImage(Long userId, String bizType, String imageUrl, String existingUrl) { + auditProfileImage(userId, bizType, imageUrl, existingUrl, safeProfileRegionId(userId)); + } + + private String safeProfileRegionId(Long userId) { + try { + IeUserProfile profile = findProfileByUserId(userId); + return profile == null ? null : profile.getRegionId(); + } catch (Exception e) { + return null; + } + } + + private void auditProfileImage(Long userId, String bizType, String imageUrl, String existingUrl, String regionId) { if (imageUrl == null || imageUrl.trim().isEmpty() || imageUrl.equals(existingUrl)) { return; } - IeAliyunModerationResult result = aliyunModerationService.checkImage(imageUrl); + IeAliyunModerationResult result = aliyunModerationService.checkImage(imageUrl, regionId, bizType, String.valueOf(userId), userId); if (result.isBlock()) { contentAuditLogService.recordAliyunBlock(bizType, userId, userId, imageUrl, result); throw new RuntimeException("图片包含不适合展示的内容,请更换一张"); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMomentServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMomentServiceImpl.java index 638da67e..325c7cd0 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMomentServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMomentServiceImpl.java @@ -84,10 +84,11 @@ public class IeMomentServiceImpl implements IeMomentService { if (!images.isEmpty() && videoUrl != null) { throw new RuntimeException("图片和视频不能同时发布"); } + String regionId = profileRegionId(userId); // 文字同步审核,取过滤后的文案 if (CharSequenceUtil.isNotBlank(content)) { - IeAuditResult audit = auditService.audit("moment", null, userId, content); + IeAuditResult audit = auditService.audit("moment", null, userId, content, regionId); if (Boolean.TRUE.equals(audit.getBlocked())) { throw new RuntimeException("文字内容包含不适合展示的表达,换一种说法吧"); } @@ -95,11 +96,11 @@ public class IeMomentServiceImpl implements IeMomentService { } // 图片同步审核,逐张过 for (int i = 0; i < images.size(); i++) { - checkImageOrThrow(userId, "moment-image", null, images.get(i), "第" + (i + 1) + "张图片包含不适合展示的内容,请更换"); + checkImageOrThrow(userId, "moment-image", null, images.get(i), "第" + (i + 1) + "张图片包含不适合展示的内容,请更换", regionId); } // 视频封面也是用户上传的图,同步先审 if (videoPoster != null) { - checkImageOrThrow(userId, "moment-video-poster", null, videoPoster, "视频封面包含不适合展示的内容,请更换"); + checkImageOrThrow(userId, "moment-video-poster", null, videoPoster, "视频封面包含不适合展示的内容,请更换", regionId); } IeMoment moment = new IeMoment(); @@ -177,7 +178,8 @@ public class IeMomentServiceImpl implements IeMomentService { blockMoment(moment, "moment video audit timeout (submit phase)"); return; } - IeAliyunModerationResult result = aliyunModerationService.submitVideo(moment.getVideoUrl()); + IeAliyunModerationResult result = aliyunModerationService.submitVideo(moment.getVideoUrl(), + profileRegionId(moment.getUserId()), "moment-video", String.valueOf(moment.getId()), moment.getUserId()); if (!result.isPending() || CharSequenceUtil.isBlank(result.getTaskId())) { // 提交失败多为瞬时问题,延迟重试;超过 10 分钟仍未成功由上面的超时判断收口 log.warn("i/e动态视频审核提交未成功,{}秒后重试 momentId={}, detail={}", @@ -206,7 +208,8 @@ public class IeMomentServiceImpl implements IeMomentService { blockMoment(moment, "moment video audit timeout"); return; } - IeAliyunModerationResult result = aliyunModerationService.queryVideoResult(moment.getAuditTaskId()); + IeAliyunModerationResult result = aliyunModerationService.queryVideoResult(moment.getAuditTaskId(), + profileRegionId(moment.getUserId()), "moment-video", String.valueOf(moment.getId()), moment.getUserId()); if (result.isPending()) { chatEventProducer.sendMomentAuditPoll(momentId, VIDEO_AUDIT_POLL_MILLIS); return; @@ -256,7 +259,12 @@ public class IeMomentServiceImpl implements IeMomentService { } private void checkImageOrThrow(Long userId, String bizType, Long bizId, String imageUrl, String blockMessage) { - IeAliyunModerationResult result = aliyunModerationService.checkImage(imageUrl); + checkImageOrThrow(userId, bizType, bizId, imageUrl, blockMessage, profileRegionId(userId)); + } + + private void checkImageOrThrow(Long userId, String bizType, Long bizId, String imageUrl, String blockMessage, String regionId) { + IeAliyunModerationResult result = aliyunModerationService.checkImage(imageUrl, regionId, bizType, + bizId == null ? null : String.valueOf(bizId), userId); if (result.isBlock()) { contentAuditLogService.recordAliyunBlock(bizType, bizId, userId, imageUrl, result); throw new RuntimeException(blockMessage); @@ -266,6 +274,21 @@ public class IeMomentServiceImpl implements IeMomentService { } } + private String profileRegionId(Long userId) { + if (userId == null) { + return null; + } + try { + IeUserProfile profile = userProfileMapper.selectOne(new LambdaQueryWrapper() + .eq(IeUserProfile::getUserId, userId) + .last("limit 1")); + return profile == null ? null : profile.getRegionId(); + } catch (Exception e) { + log.warn("查询i/e动态用户校区失败,不影响阿里云计费记录 userId={}", userId, e); + return null; + } + } + private List normalizeImages(List images) { List result = new ArrayList<>(); if (images == null) { diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRealNameAuthServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRealNameAuthServiceImpl.java index be2637db..b05083a0 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRealNameAuthServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRealNameAuthServiceImpl.java @@ -3,6 +3,7 @@ package cc.hiver.mall.ie.service.impl; import cc.hiver.core.common.redis.RedisTemplateHelper; import cc.hiver.core.entity.User; import cc.hiver.core.service.UserService; +import cc.hiver.mall.billing.mq.BillingRecordProducer; import cc.hiver.mall.ie.constant.IeConstants; import cc.hiver.mall.ie.entity.IeUserProfile; import cc.hiver.mall.ie.exception.IeBusinessException; @@ -79,6 +80,9 @@ public class IeRealNameAuthServiceImpl implements IeRealNameAuthService { @Autowired private RedisTemplateHelper redisTemplateHelper; + @Autowired(required = false) + private BillingRecordProducer billingRecordProducer; + /** * 阿里云身份证三要素核验 AccessKeyId。 * 建议生产环境使用 RAM 子账号,并仅授予 dytns:CertNoThreeElementVerification 权限。 @@ -139,7 +143,7 @@ public class IeRealNameAuthServiceImpl implements IeRealNameAuthService { } // 阿里云返回 Data.IsConsistent = "1" 才代表姓名、身份证号、人像照片为同一人。 - verifyResult = callAliyunThreeElement(normalizedName, normalizedCertNo, normalizedPicture); + verifyResult = callAliyunThreeElement(normalizedName, normalizedCertNo, normalizedPicture, normalizedRegionId, userId); if (!"1".equals(verifyResult.getIsConsistent())) { throw new IeBusinessException(aliyunResultMessage(verifyResult.getIsConsistent())); } @@ -260,7 +264,8 @@ public class IeRealNameAuthServiceImpl implements IeRealNameAuthService { redisTemplateHelper.set(REAL_NAME_AUDIT_ENABLED_KEY, Boolean.FALSE.equals(enabled) ? "0" : "1"); } - private AliyunThreeElementResult callAliyunThreeElement(String certName, String certNo, String certPicture) { + private AliyunThreeElementResult callAliyunThreeElement(String certName, String certNo, String certPicture, + String regionId, Long userId) { if (isBlank(aliyunAccessKeyId) || isBlank(aliyunAccessKeySecret) || isBlank(aliyunAuthCode)) { throw new IeBusinessException("未配置阿里云身份证三要素核验参数"); } @@ -296,21 +301,33 @@ public class IeRealNameAuthServiceImpl implements IeRealNameAuthService { String code = json.getStr("Code", String.valueOf(response.code())); String requestId = json.getStr("RequestId", ""); String message = json.getStr("Message", "请稍后重试"); + recordRealNameBilling(regionId, userId, "failed", requestId, + "code=" + code + ", message=" + message); throw new IeBusinessException("阿里云实名认证失败:" + message + ",错误码:" + code + ",RequestId:" + requestId); } JSONObject data = json.getJSONObject("Data"); AliyunThreeElementResult result = new AliyunThreeElementResult(); result.setIsConsistent(data == null ? null : data.getStr("IsConsistent")); result.setRequestId(json.getStr("RequestId")); + recordRealNameBilling(regionId, userId, "1".equals(result.getIsConsistent()) ? "pass" : "failed", + result.getRequestId(), "isConsistent=" + result.getIsConsistent()); return result; } } catch (IeBusinessException e) { throw e; } catch (Exception e) { + recordRealNameBilling(regionId, userId, "failed", null, e.getMessage()); throw new IeBusinessException("调用阿里云身份证三要素核验接口失败", e); } } + private void recordRealNameBilling(String regionId, Long userId, String resultStatus, String requestId, String detail) { + if (billingRecordProducer != null) { + billingRecordProducer.recordRealNameAuth(regionId, userId == null ? null : String.valueOf(userId), + resultStatus, requestId, detail); + } + } + private String signAliyunRpc(Map params) throws Exception { List keys = new ArrayList<>(params.keySet()); Collections.sort(keys); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeSecurityAuditServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeSecurityAuditServiceImpl.java index bac961e4..e4a3494b 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeSecurityAuditServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeSecurityAuditServiceImpl.java @@ -43,7 +43,7 @@ public class IeSecurityAuditServiceImpl implements IeSecurityAuditService { "博彩", "赌博", "下注", "开盘", "卖号", "买号", "盗号", "洗钱", "跑分", "代实名", "出售银行卡", "收银行卡", "身份证代办", "发定位", "发照片", "要照片", "真实姓名", "宿舍号", "寝室号", "线下见面", "来我宿舍", "去你宿舍", "约吗", "微信", "法轮功", "草一下", "c一下", "艹一下", "干一下", "逼", - "几把", "鸡吧" + "几把", "鸡吧", "代课" ); private volatile SensitiveWordBs cachedWordBs; @@ -63,7 +63,7 @@ public class IeSecurityAuditServiceImpl implements IeSecurityAuditService { private IeAliyunModerationService aliyunModerationService; @Override - public IeAuditResult audit(String bizType, Long bizId, Long userId, String content) { + public IeAuditResult audit(String bizType, Long bizId, Long userId, String content, String regionId) { IeAuditResult result = new IeAuditResult(); result.setRawContent(content); result.setFilteredContent(content); @@ -121,7 +121,8 @@ public class IeSecurityAuditServiceImpl implements IeSecurityAuditService { // 本地词库没拦住的,再过一遍阿里云文本审核;服务异常时降级为本地结果,不阻断 if (!Boolean.TRUE.equals(result.getBlocked())) { - IeAliyunModerationResult aliResult = aliyunModerationService.checkText(content); + IeAliyunModerationResult aliResult = aliyunModerationService.checkText(content, regionId, bizType, + bizId == null ? null : String.valueOf(bizId), userId); if (aliResult.isBlock()) { result.setAuditStatus(IeConstants.AUDIT_BLOCK); result.setRiskLevel(IeConstants.RISK_HIGH); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncConsumer.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncConsumer.java index 973515d8..afe111a4 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncConsumer.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncConsumer.java @@ -4,6 +4,7 @@ import cc.hiver.core.entity.User; import cc.hiver.core.entity.Worker; import cc.hiver.core.service.UserService; import cc.hiver.core.service.WorkerService; +import cc.hiver.core.serviceimpl.JPushServiceImpl; import cc.hiver.mall.dao.mapper.MallDeliveryOrderMapper; import cc.hiver.mall.dao.mapper.MallOrderMapper; import cc.hiver.mall.entity.MallDeliveryOrder; @@ -54,6 +55,9 @@ public class OrderAsyncConsumer { @Autowired private WechatSendMessageUtil wechatSendMessageUtil; + @Autowired + private JPushServiceImpl jPushService; + @Autowired private UserPendingOrderCacheUtil userPendingOrderCacheUtil; @@ -79,7 +83,7 @@ public class OrderAsyncConsumer { private PlanetRewardHook planetRewardHook; // 微信模板配置 - private static final String TEMPLATE_ID = "K15zpZSHBNivouTfpW5FK1XFz8GbYAK8b9dXXOP_Ka0"; + private static final String TEMPLATE_ID = "Cb2HLjqub1deI_7JjqLgmH58tIsi3alniDO5wseFzn4"; @RabbitListener(queues = OrderQueueConfig.ASYNC_WORKER_QUEUE) public void handleWechatNotify(String message) { @@ -112,36 +116,44 @@ public class OrderAsyncConsumer { } targetOpenIds = getOfficialAccountOpenids(targetWorkers); - if (targetOpenIds.isEmpty()) { + if (!targetOpenIds.isEmpty()) { + // 组装模板参数 (参照要求配置) + List data = new ArrayList<>(); + Integer goodsCount = delivery.getAllCount() != null && delivery.getAllCount() > 0 ? delivery.getAllCount() : 1; + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + String timeFormat = delivery.getCreateTime() != null ? sdf.format(delivery.getCreateTime()) : sdf.format(new java.util.Date()); + String orderTypeValue = orderTypeStr + " " + goodsCount + "件商品"; + if (delivery.getMustFinishTime() != null) { + orderTypeValue = orderTypeValue + " " + new SimpleDateFormat("HH:mm").format(delivery.getMustFinishTime()) + "前"; + } + data.add(new WxMpTemplateData("thing8", orderTypeValue)); + data.add(new WxMpTemplateData("time5", timeFormat)); + + String address = StringUtils.defaultString(delivery.getReceiverAddress()); + // 模板参数有长度限制(通常20字),做简略截断以防发送失败 + if (address.length() > 20) { + address = address.substring(0, 17) + "..."; + } + data.add(new WxMpTemplateData("thing10", address)); + + String shopName = StringUtils.defaultString(delivery.getShopName()); + if (shopName.length() > 20) { + shopName = shopName.substring(0, 17) + "..."; + } + data.add(new WxMpTemplateData("thing2", shopName)); + + wechatSendMessageUtil.sendWechatTempMessage(targetOpenIds, TEMPLATE_ID, data, "pages/index/index"); + log.info("【异步队列处理】微信模版推送成功,推送总人数:{}", targetOpenIds.size()); + } else { log.info("【异步队列处理】没有找到可推送微信模板的openid,目标跳过推送。orderId={}", orderId); - return; } - // 组装模板参数 (参照要求配置) - List data = new ArrayList<>(); - data.add(new WxMpTemplateData("thing3", orderTypeStr)); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - String timeFormat = delivery.getCreateTime() != null ? sdf.format(delivery.getCreateTime()) : sdf.format(new java.util.Date()); - data.add(new WxMpTemplateData("time4", timeFormat)); - - String address = StringUtils.defaultString(delivery.getShopName()) + " - " + StringUtils.defaultString(delivery.getReceiverAddress()); - // 模板参数有长度限制(通常20字),做简略截断以防发送失败 - if (address.length() > 20) { - address = address.substring(0, 17) + "..."; - } - data.add(new WxMpTemplateData("thing10", address)); - - // 查询或者组装此单据的物品数量,delivery可能没有存数量,这里可简化显示。 - // 由于MallDeliveryOrder中可能没存总量,可通过orderId反查Goods(篇幅及耗时原因,也可以在发送消息时传进来) - // 查出 MallDeliveryOrder 自带或者去 MallOrderGood - data.add(new WxMpTemplateData("character_string15", "1")); // 也可以去 mall_order_goods 查数量 - - data.add(new WxMpTemplateData("phrase8", "待接单")); - - wechatSendMessageUtil.sendWechatTempMessage(targetOpenIds, TEMPLATE_ID, data, "pages/index/index"); - log.info("【异步队列处理】微信模版推送成功,推送总人数:{}", targetOpenIds.size()); + String pushMessage = StringUtils.isNotBlank(delivery.getWorkerId()) + ? "您有一笔新的指定配送单,请及时接单" + : "抢单大厅来新单啦"; + sendJPushToWorkers(targetWorkers, pushMessage, orderId); } catch (Exception e) { - log.error("【异步队列处理】微信推送异常(不影响主链路): {}", e.getMessage(), e); + log.error("【异步队列处理】配送订单通知推送异常(不影响主链路): {}", e.getMessage(), e); throw new org.springframework.amqp.AmqpRejectAndDontRequeueException(e); } } @@ -152,7 +164,7 @@ public class OrderAsyncConsumer { return null; } Worker worker = workerRedisVo.getWorker(); - if (!isOnlineWorkerInRegion(worker, regionId) || StringUtils.isBlank(worker.getUserId())) { + if (!isOnlineWorkerInRegion(worker, regionId)) { return null; } return worker; @@ -205,6 +217,30 @@ public class OrderAsyncConsumer { return openids; } + private void sendJPushToWorkers(List workers, String message, String orderId) { + if (workers == null || workers.isEmpty()) { + return; + } + Set clientIds = new LinkedHashSet<>(); + for (Worker worker : workers) { + if (worker != null && StringUtils.isNotBlank(worker.getClientId())) { + clientIds.add(worker.getClientId()); + } + } + if (clientIds.isEmpty()) { + log.info("【异步队列处理】没有找到可推送极光的clientId,目标跳过推送。orderId={}", orderId); + return; + } + for (String clientId : clientIds) { + try { + jPushService.sendPushNotification(clientId, message, orderId); + } catch (Exception e) { + log.warn("【异步队列处理】极光推送失败,orderId={}, clientId={}, err={}", orderId, clientId, e.getMessage()); + } + } + log.info("【异步队列处理】极光推送完成,推送总人数:{},orderId={}", clientIds.size(), orderId); + } + private boolean isOnlineWorkerInRegion(Worker worker, String regionId) { return worker != null && StringUtils.equals(worker.getRegion(), regionId) diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderDelayConsumer.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderDelayConsumer.java index 1e2219e1..f499b21e 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderDelayConsumer.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderDelayConsumer.java @@ -3,6 +3,7 @@ package cc.hiver.mall.mq; import cc.hiver.core.common.constant.SettingConstant; import cc.hiver.core.common.sms.SmsUtil; import cc.hiver.core.serviceimpl.JPushServiceImpl; +import cc.hiver.mall.billing.mq.BillingRecordProducer; import cc.hiver.mall.dao.mapper.MallDeliveryOrderMapper; import cc.hiver.mall.dao.mapper.MallOrderMapper; import cc.hiver.mall.dao.mapper.ShopMapper; @@ -48,6 +49,8 @@ public class OrderDelayConsumer { @Autowired private SmsUtil smsUtil; + @Autowired(required = false) + private BillingRecordProducer billingRecordProducer; @Autowired private JPushServiceImpl jPushService; @@ -124,6 +127,8 @@ public class OrderDelayConsumer { // 发送 SMS_TIMEOUT if (StringUtils.isNotBlank(delivery.getWorkerPhone())) { smsUtil.sendCode(delivery.getWorkerPhone(), null,SettingConstant.SMS_TYPE.SMS_TIMEOUT.name()); + recordSmsBilling(delivery.getRegionId(), SettingConstant.SMS_TYPE.SMS_TIMEOUT.name(), + delivery.getWorkerPhone(), "mall_delivery_order", delivery.getId()); } } @@ -148,6 +153,8 @@ public class OrderDelayConsumer { // 给用户通知 if (StringUtils.isNotBlank(delivery.getReceiverPhone())) { smsUtil.sendCode(delivery.getReceiverPhone(), null,SettingConstant.SMS_TYPE.SMS_NO_ORDER.name()); + recordSmsBilling(delivery.getRegionId(), SettingConstant.SMS_TYPE.SMS_NO_ORDER.name(), + delivery.getReceiverPhone(), "mall_delivery_order", delivery.getId()); } // 给商家通知 if (StringUtils.isNotBlank(delivery.getShopPhone()) && delivery.getDeliveryType() == 1) { @@ -198,6 +205,8 @@ public class OrderDelayConsumer { log.info("【订单延时处理】大厅抢单 10分钟无人接单,通知用户 orderId={}", orderId); if (StringUtils.isNotBlank(delivery.getReceiverPhone())) { smsUtil.sendCode(delivery.getReceiverPhone(),null, SettingConstant.SMS_TYPE.SMS_NO_ORDER.name()); + recordSmsBilling(delivery.getRegionId(), SettingConstant.SMS_TYPE.SMS_NO_ORDER.name(), + delivery.getReceiverPhone(), "mall_delivery_order", delivery.getId()); } // 给商家通知 if (StringUtils.isNotBlank(delivery.getShopPhone()) && delivery.getDeliveryType() == 1) { @@ -268,6 +277,8 @@ public class OrderDelayConsumer { } log.info("【订单延时处理】中转单商家送达10分钟后校内兼职未取货,发送短信提醒 orderId={}", orderId); smsUtil.sendCode(delivery.getWorkerPhone(), null, SettingConstant.SMS_TYPE.SMS_TRANSFER.name()); + recordSmsBilling(delivery.getRegionId(), SettingConstant.SMS_TYPE.SMS_TRANSFER.name(), + delivery.getWorkerPhone(), "mall_delivery_order", delivery.getId()); } private MallDeliveryOrder getDeliveryOrder(String orderId) { @@ -276,6 +287,12 @@ public class OrderDelayConsumer { return mallDeliveryOrderMapper.selectOne(qw); } + private void recordSmsBilling(String regionId, String smsType, String mobile, String bizType, String bizId) { + if (billingRecordProducer != null) { + billingRecordProducer.recordSms(regionId, smsType, mobile, bizType, bizId); + } + } + /** * 退款/售后申请1小时未处理,自动默认同意 * diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java index 67b96ef5..93b9c8fd 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java @@ -5,8 +5,10 @@ import cc.hiver.core.common.constant.SettingConstant; import cc.hiver.core.common.redis.RedisTemplateHelper; import cc.hiver.core.common.sms.SmsUtil; import cc.hiver.core.entity.Worker; +import cc.hiver.core.service.WorkerService; import cc.hiver.core.serviceimpl.JPushServiceImpl; import cc.hiver.core.serviceimpl.WorkerServiceImpl; +import cc.hiver.mall.billing.mq.BillingRecordProducer; import cc.hiver.mall.controller.AdminSeckillGroupController; import cc.hiver.mall.dao.mapper.*; import cc.hiver.mall.entity.*; @@ -16,8 +18,6 @@ import cc.hiver.mall.pojo.query.MallDeliveryOrderPageQuery; import cc.hiver.mall.pojo.query.MallRefundRecordPageQuery; import cc.hiver.mall.pojo.vo.MallOrderFreeVO; import cc.hiver.mall.pojo.vo.MallOrderVO; -import cc.hiver.mall.service.CommentService; -import cc.hiver.mall.service.ShopService; import cc.hiver.mall.service.mybatis.*; import cc.hiver.mall.utils.*; import cn.hutool.json.JSONUtil; @@ -85,9 +85,8 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl goodsList = delivery.getGoodsList() == null ? new ArrayList<>() : new ArrayList<>(delivery.getGoodsList()); // 提前查好 Shop,避免重复查询(后续推送复用) Shop cachedShop = null; - String pushClientId = null; + Map pushClientIdMap = new HashMap<>(); if (order != null && StringUtils.isNotBlank(order.getShopId())) { cachedShop = getShopFromCache(order.getRegionId(), order.getShopId()); - pushClientId = cachedShop != null ? cachedShop.getClientId() : null; + if (cachedShop != null && StringUtils.isNotBlank(cachedShop.getClientId()) && StringUtils.isNotBlank(order.getId())) { + pushClientIdMap.put(order.getId(), cachedShop.getClientId()); + } } // 推送 orderId 列表,待事务提交后异步推送 List pushOrderIds = new ArrayList<>(); @@ -409,6 +398,10 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl finalPushClientIdMap = new HashMap<>(pushClientIdMap); + final List finalPushOrderIds = new ArrayList<>(pushOrderIds); + if (Boolean.TRUE.equals(notifyShop) && !finalPushClientIdMap.isEmpty() && !finalPushOrderIds.isEmpty()) { new Thread(() -> { - try { - jPushService.sendPushNotification(finalPushClientId, "您有一笔新的订单", firstOrderId); - } catch (Exception e) { - log.warn("极光推送失败, orderId={}, err={}", firstOrderId, e.getMessage()); + for (String orderId : finalPushOrderIds) { + String clientId = finalPushClientIdMap.get(orderId); + if (StringUtils.isBlank(clientId)) { + continue; + } + try { + jPushService.sendPushNotification(clientId, "您有一笔新的订单", orderId); + } catch (Exception e) { + log.warn("极光推送失败, orderId={}, err={}", orderId, e.getMessage()); + } } }).start(); } @@ -509,6 +508,14 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl records) { if (records == null || records.isEmpty()) { return; @@ -1290,6 +1305,12 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl selectMallGroup(MallOrderGroup group) { return this.baseMapper.selectMallGroup(group); } + + private void recordSmsBilling(String regionId, String smsType, String mobile, String bizType, String bizId) { + if (billingRecordProducer != null) { + billingRecordProducer.recordSms(regionId, smsType, mobile, bizType, bizId); + } + } } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java index 12745778..aa39d3cc 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java @@ -7,8 +7,11 @@ import cc.hiver.core.common.sms.SmsUtil; import cc.hiver.core.common.utils.SecurityUtil; import cc.hiver.core.common.utils.SnowFlakeUtil; import cc.hiver.core.entity.User; +import cc.hiver.core.entity.Worker; import cc.hiver.core.service.UserService; +import cc.hiver.core.service.WorkerService; import cc.hiver.core.serviceimpl.JPushServiceImpl; +import cc.hiver.mall.billing.mq.BillingRecordProducer; import cc.hiver.mall.controller.AdminSeckillGroupController; import cc.hiver.mall.dao.mapper.*; import cc.hiver.mall.entity.*; @@ -102,6 +105,9 @@ public class MallOrderServiceImpl extends ServiceImpl qw = new LambdaQueryWrapper<>(); qw.eq(MallDeliveryOrder::getOrderId, order.getId()); MallDeliveryOrder delivery = mallDeliveryOrderMapper.selectOne(qw); + if(delivery.getWorkerId() != null){ + Worker worker = workerService.findByWorkerId(delivery.getWorkerId()); + if(worker.getClientId() != null){ + jPushService.sendPushNotification(worker.getClientId(), "您有一笔配送中订单用户申请退款", order.getId()); + } + } smsUtil.sendCode(delivery.getWorkerPhone(), null, SettingConstant.SMS_TYPE.SMS_ORDERRETURN.name()); + recordSmsBilling(delivery.getRegionId(), SettingConstant.SMS_TYPE.SMS_ORDERRETURN.name(), + delivery.getWorkerPhone(), "mall_order", order.getId()); //更新缓存 MallDeliveryOrder delivery1 = workerOrderCacheUtil.get(workerId,delivery.getId()); if(delivery1 != null){ @@ -2890,4 +2920,10 @@ public class MallOrderServiceImpl extends ServiceImpl