Browse Source

对接拼团数据1

master
wangfukang 2 days ago
parent
commit
c661eeefcb
  1. 16
      hiver-admin/test-output/test-report.html
  2. 115
      hiver-modules/hiver-base/src/main/java/cc/hiver/base/controller/manage/AuthController.java
  3. 32
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/entity/BillingUsageRecord.java
  4. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mapper/BillingUsageRecordMapper.java
  5. 50
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordConsumer.java
  6. 26
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordEvent.java
  7. 31
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordMqConfig.java
  8. 79
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/billing/mq/BillingRecordProducer.java
  9. 150
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminBillingUsageController.java
  10. 299
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminDataStatisticsController.java
  11. 213
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/AdminSettlementController.java
  12. 103
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ReturnCommissionController.java
  13. 16
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/WorkerRelaPriceController.java
  14. 2
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/ReturnCommission.java
  15. 7
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/controller/FishMarketAdminController.java
  16. 24
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/controller/FishMarketController.java
  17. 2
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/service/FishMarketService.java
  18. 302
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/fishmarket/service/impl/FishMarketServiceImpl.java
  19. 16
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRecordMapper.java
  20. 36
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeAliyunModerationService.java
  21. 6
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeSecurityAuditService.java
  22. 135
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeAliyunModerationServiceImpl.java
  23. 128
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java
  24. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeDailyQuestionServiceImpl.java
  25. 42
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java
  26. 35
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMomentServiceImpl.java
  27. 21
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRealNameAuthServiceImpl.java
  28. 7
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeSecurityAuditServiceImpl.java
  29. 92
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderAsyncConsumer.java
  30. 17
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/mq/OrderDelayConsumer.java
  31. 78
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallDeliveryOrderServiceImpl.java
  32. 18
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderGroupServiceImpl.java
  33. 36
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java
  34. 13
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallRefundRecordServiceImpl.java
  35. 21
      hiver-modules/hiver-mall/src/main/resources/db/billing_usage_record.sql
  36. 8
      hiver-modules/hiver-mall/src/main/resources/db/ie_records_page_indexes.sql

16
hiver-admin/test-output/test-report.html

@ -35,7 +35,7 @@
<a href="#"><span class="badge badge-primary">Hiver</span></a>
</li>
<li class="m-r-10">
<a href="#"><span class="badge badge-primary">七月 22, 2026 09:13:14</span></a>
<a href="#"><span class="badge badge-primary">七月 26, 2026 17:19:19</span></a>
</li>
</ul>
</div>
@ -84,7 +84,7 @@
<div class="test-detail">
<span class="meta text-white badge badge-sm"></span>
<p class="name">passTest</p>
<p class="text-sm"><span>09:13:14 上</span> / <span>0.021 secs</span></p>
<p class="text-sm"><span>17:19:19 下</span> / <span>0.023 secs</span></p>
</div>
<div class="test-contents d-none">
<div class="detail-head">
@ -92,9 +92,9 @@
<div class="info">
<div class='float-right'><span class='badge badge-default'>#test-id=1</span></div>
<h5 class="test-status text-pass">passTest</h5>
<span class='badge badge-success'>07.22.2026 09:13:14</span>
<span class='badge badge-danger'>07.22.2026 09:13:14</span>
<span class='badge badge-default'>0.021 secs</span>
<span class='badge badge-success'>07.26.2026 17:19:19</span>
<span class='badge badge-danger'>07.26.2026 17:19:19</span>
<span class='badge badge-default'>0.023 secs</span>
</div>
<div class="m-t-10 m-l-5"></div>
</div>
@ -104,7 +104,7 @@
<tbody>
<tr class="event-row">
<td><span class="badge log pass-bg">Pass</span></td>
<td>9:13:14</td>
<td>17:19:19</td>
<td>
Test passed
</td>
@ -128,13 +128,13 @@
<div class="col-md-3">
<div class="card"><div class="card-body">
<p class="m-b-0">Started</p>
<h3>七月 22, 2026 09:13:14</h3>
<h3>七月 26, 2026 17:19:19</h3>
</div></div>
</div>
<div class="col-md-3">
<div class="card"><div class="card-body">
<p class="m-b-0">Ended</p>
<h3>七月 22, 2026 09:13:14</h3>
<h3>七月 26, 2026 17:19:19</h3>
</div></div>
</div>
<div class="col-md-3">

115
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<ShopUser> 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> shop = shopService.getShopByUserid(user.getId());
if(!shop.isEmpty()){
List<String> 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> 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<WorkerRelaPrice> workerRelaPriceList = workerRelaPriceService.selectByWorkerId(worker.getWorkerId());
List<WorkerRelaPrice> workerRelaPriceListWaimai = new ArrayList<>();
List<WorkerRelaPrice> 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);
}

32
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;
}

9
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<BillingUsageRecord> {
}

50
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);
}
}
}

26
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;
}

31
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);
}
}

79
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);
}
}

150
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<Page<BillingUsageSummaryVo>> summaryList(@RequestBody BillingUsageQuery query) {
QueryWrapper<BillingUsageRecord> 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<Map<String, Object>> rowPage = billingUsageRecordMapper.selectMapsPage(
new Page<>(Math.max(query.getPageNumber(), 1), Math.max(query.getPageSize(), 10)), wrapper);
List<BillingUsageSummaryVo> records = new ArrayList<>();
for (Map<String, Object> 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<BillingUsageSummaryVo> result = new Page<>(rowPage.getCurrent(), rowPage.getSize(), rowPage.getTotal());
result.setRecords(records);
return new ResultUtil<Page<BillingUsageSummaryVo>>().setData(result);
}
@PostMapping("/detailList")
@ApiOperation(value = "查询第三方计费用量明细")
public Result<Page<BillingUsageRecord>> detailList(@RequestBody BillingUsageQuery query) {
LambdaQueryWrapper<BillingUsageRecord> 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<BillingUsageRecord> page = new Page<>(Math.max(query.getPageNumber(), 1), Math.max(query.getPageSize(), 10));
return new ResultUtil<Page<BillingUsageRecord>>().setData(billingUsageRecordMapper.selectPage(page, wrapper));
}
private void applyMapConditions(QueryWrapper<BillingUsageRecord> 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));
}
}

299
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<DataStatisticsSummaryVo> summary(@RequestBody DataStatisticsQuery query) {
DateRange range = resolveDateRange(query);
String regionId = query == null ? null : query.getRegionId();
List<MallOrder> orders = mallOrderMapper.selectList(new LambdaQueryWrapper<MallOrder>()
.eq(StringUtils.isNotBlank(regionId), MallOrder::getRegionId, regionId)
.ge(MallOrder::getPayTime, range.start)
.le(MallOrder::getPayTime, range.end)
.notIn(MallOrder::getStatus, 0, 6));
List<MallRefundRecord> refunds = mallRefundRecordMapper.selectList(new LambdaQueryWrapper<MallRefundRecord>()
.eq(StringUtils.isNotBlank(regionId), MallRefundRecord::getRegionId, regionId)
.ge(MallRefundRecord::getSuccessTime, range.start)
.le(MallRefundRecord::getSuccessTime, range.end)
.in(MallRefundRecord::getStatus, 1, 4));
List<MallDeliveryOrder> deliveries = mallDeliveryOrderMapper.selectList(new LambdaQueryWrapper<MallDeliveryOrder>()
.eq(StringUtils.isNotBlank(regionId), MallDeliveryOrder::getRegionId, regionId)
.ge(MallDeliveryOrder::getFinishTime, range.start)
.le(MallDeliveryOrder::getFinishTime, range.end)
.isNotNull(MallDeliveryOrder::getFinishTime));
List<MallUserCoupon> coupons = mallUserCouponMapper.selectList(new LambdaQueryWrapper<MallUserCoupon>()
.eq(StringUtils.isNotBlank(regionId), MallUserCoupon::getRegionId, regionId)
.in(MallUserCoupon::getStatus, 1,2)
.ge(MallUserCoupon::getUseTime, range.start)
.le(MallUserCoupon::getUseTime, range.end));
List<DealingsRecord> rewardRecords = dealingsRecordMapper.selectList(new LambdaQueryWrapper<DealingsRecord>()
.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<PlanetPool> pools = planetPoolMapper.selectList(new LambdaQueryWrapper<PlanetPool>()
.eq(StringUtils.isNotBlank(regionId), PlanetPool::getRegionId, regionId)
.ge(PlanetPool::getDrawTime, range.start)
.le(PlanetPool::getDrawTime, range.end));
List<BillingUsageRecord> billingRecords = billingUsageRecordMapper.selectList(new LambdaQueryWrapper<BillingUsageRecord>()
.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<String> shopIds = new HashSet<>();
Set<String> 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<DataStatisticsSummaryVo>().setData(vo);
}
private BigDecimal sumRefundAmount(List<MallRefundRecord> 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<DealingsRecord> 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<MallDeliveryOrder> 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;
}
}

213
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<MallSettlementRecord> qw = new LambdaQueryWrapper<>();
qw.eq(MallSettlementRecord::getRegionId, query.getRegionId());
qw.eq(MallSettlementRecord::getStatus, query.getStatus() == null ? 0 : query.getStatus());
QueryWrapper<MallSettlementRecord> 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<MallSettlementRecord> list = mallSettlementRecordService.list(qw);
// Group by shopId
Map<String, List<MallSettlementRecord>> map = list.stream().collect(Collectors.groupingBy(MallSettlementRecord::getShopId));
List<String> shopIds = map.keySet().stream()
Page<Map<String, Object>> rowPage = mallSettlementRecordService.pageMaps(
new Page<>(pageNumber(query), pageSize(query)), qw);
List<String> shopIds = rowPage.getRecords().stream()
.map(row -> stringValue(row.get("shop_id")))
.filter(StringUtils::isNotBlank)
.collect(Collectors.toList());
Map<String, Shop> shopMap = shopIds.isEmpty() ? new java.util.HashMap<>() : shopService.getRepository().findAllById(shopIds).stream()
.collect(Collectors.toMap(Shop::getId, Function.identity(), (a, b) -> a));
List<SettlementSummaryVo> result = new ArrayList<>();
for (Map.Entry<String, List<MallSettlementRecord>> entry : map.entrySet()) {
String shopId = entry.getKey();
List<MallSettlementRecord> shopRecords = entry.getValue();
List<SettlementSummaryVo> records = new java.util.ArrayList<>();
for (Map<String, Object> 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<Page<SettlementSummaryVo>>().setData(buildPage(result, query));
Page<SettlementSummaryVo> result = new Page<>(rowPage.getCurrent(), rowPage.getSize(), rowPage.getTotal());
result.setRecords(records);
return new ResultUtil<Page<SettlementSummaryVo>>().setData(result);
}
@Data
@ -248,43 +238,52 @@ public class AdminSettlementController {
@PostMapping("/dateSummaryList")
@ApiOperation(value = "按日期获取结算汇总")
public Result<Page<DateSummaryVo>> 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<MallSettlementRecord> qw = new LambdaQueryWrapper<>();
qw.eq(MallSettlementRecord::getRegionId, query.getRegionId());
qw.eq(MallSettlementRecord::getStatus, query.getStatus() == null ? 0 : query.getStatus());
QueryWrapper<MallSettlementRecord> 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<MallSettlementRecord> list = mallSettlementRecordService.list(qw);
Map<String, List<MallSettlementRecord>> byDate = list.stream()
.collect(Collectors.groupingBy(r -> new java.text.SimpleDateFormat("yyyy-MM-dd").format(r.getCreateTime())));
List<DateSummaryVo> result = new ArrayList<>();
for (Map.Entry<String, List<MallSettlementRecord>> entry : byDate.entrySet()) {
qw.groupBy("DATE(create_time)");
qw.orderByDesc("bill_time");
Page<Map<String, Object>> rowPage = mallSettlementRecordService.pageMaps(
new Page<>(pageNumber(query), pageSize(query)), qw);
List<DateSummaryVo> records = new java.util.ArrayList<>();
for (Map<String, Object> 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<Page<DateSummaryVo>>().setData(buildPage(result, query));
}
private <T> Page<T> buildPage(List<T> 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<T> page = new Page<>(pageNumber, pageSize, list.size());
page.setRecords(list.subList(fromIndex, toIndex));
return page;
Page<DateSummaryVo> result = new Page<>(rowPage.getCurrent(), rowPage.getSize(), rowPage.getTotal());
result.setRecords(records);
return new ResultUtil<Page<DateSummaryVo>>().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<List<MallSettlementRecord>> detailList(@RequestBody DetailQuery query) {
public Result<Page<MallSettlementRecord>> detailList(@RequestBody DetailQuery query) {
LambdaQueryWrapper<MallSettlementRecord> 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<MallSettlementRecord> list = mallSettlementRecordService.list(qw);
Page<MallSettlementRecord> page = mallSettlementRecordService.page(
new Page<>(Math.max(query.getPageNumber(), 1), Math.max(query.getPageSize(), 10)), qw);
List<MallSettlementRecord> list = page.getRecords();
List<String> shopIds = list.stream()
.map(MallSettlementRecord::getShopId)
.filter(StringUtils::isNotBlank)
@ -340,32 +343,36 @@ public class AdminSettlementController {
record.setPayTime(order.getPayTime());
}
}
return new ResultUtil<List<MallSettlementRecord>>().setData(list);
return new ResultUtil<Page<MallSettlementRecord>>().setData(page);
}
private void fillAmounts(DateSummaryVo vo, List<MallSettlementRecord> 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

103
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());

16
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<WorkerRealPriceVo> 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<WorkerRelaPrice> workerRelaPriceList = workerRelaPriceService.selectByWorkerId(worker.getWorkerId());
//更新配送员信息
worker.setWorkerName(workerRealPriceVo.getWorkerName());

2
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;

7
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<Object> unblockComplaintUser(@RequestBody FishAdminActionDTO dto) {
fishMarketService.unblockComplaintUser(dto, currentUser());
return ResultUtil.success("解除封禁成功");
}
@RequestMapping(value = "/users/limits/page", method = RequestMethod.POST)
@ApiOperation("用户风控分页")
public Result<IPage<FishUserLimit>> userLimits(@RequestBody(required = false) FishPageQuery query) {

24
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<IPage<FishGoods>> pageGoods(@RequestBody(required = false) FishPageQuery query) {
@ -63,14 +68,18 @@ public class FishMarketController {
@RequestMapping(value = "/goods", method = RequestMethod.POST)
@ApiOperation("发布/编辑出售商品")
public Result<FishGoods> publishGoods(@RequestBody FishPublishDTO dto) {
return new ResultUtil<FishGoods>().setData(fishMarketService.publishGoods(dto, currentUser()));
public Result<Map<String, Object>> publishGoods(@RequestBody FishPublishDTO dto) {
User user = currentUser();
FishGoods goods = fishMarketService.publishGoods(dto, user);
return new ResultUtil<Map<String, Object>>().setData(publishResult(goods, user));
}
@RequestMapping(value = "/wants", method = RequestMethod.POST)
@ApiOperation("发布/编辑求购")
public Result<FishWant> publishWant(@RequestBody FishPublishDTO dto) {
return new ResultUtil<FishWant>().setData(fishMarketService.publishWant(dto, currentUser()));
public Result<Map<String, Object>> publishWant(@RequestBody FishPublishDTO dto) {
User user = currentUser();
FishWant want = fishMarketService.publishWant(dto, user);
return new ResultUtil<Map<String, Object>>().setData(publishResult(want, user));
}
@RequestMapping(value = "/images/audit", method = RequestMethod.POST)
@ -272,4 +281,11 @@ public class FishMarketController {
return null;
}
}
private Map<String, Object> publishResult(Object data, User user) {
Map<String, Object> result = new HashMap<>();
result.put("data", data);
result.put("user", userService.findById(user.getId()));
return result;
}
}

2
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<FishComplaint> adminPageComplaints(FishPageQuery query);

302
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<FishGoods> 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<String, Object> 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<FishChatSession> sessions, String userId) {
if (sessions == null || sessions.isEmpty()) return;
Set<String> 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<WxMpTemplateData> 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<String> images) {
private void auditImages(String bizType, String userId, List<String> 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<String> images) {
private void auditStrictImages(String bizType, String userId, List<String> 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<FishComplaint> complaints) {
if (complaints == null || complaints.isEmpty()) return;
List<String> goodsIds = complaints.stream()
Set<String> commentIds = complaints.stream()
.map(FishComplaint::getCommentId)
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
Map<String, FishComment> commentMap = commentIds.isEmpty() ? Collections.emptyMap() : commentMapper.selectBatchIds(commentIds).stream()
.collect(Collectors.toMap(FishComment::getId, comment -> comment, (a, b) -> a));
Set<String> 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<String, FishGoods> 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());
}
}

16
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<IeRecord> {
@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<IeRecord> pageWithUnread(Page<IeRecord> page, @Param("userId") Long userId);
}

36
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);
}

6
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);
}

135
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 延迟重试兜底

128
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<IeRecord>()
.eq(IeRecord::getRoomId, room.getId())
@ -913,47 +929,15 @@ public class IeChatServiceImpl implements IeChatService {
public IPage<IeRecord> pageRecords(Long userId, Integer pageNumber, Integer pageSize) {
long current = safePageNumber(pageNumber);
long size = safePageSize(pageSize);
Page<IeRecord> result = new Page<>(current, size);
List<IeRecord> records = recordMapper.selectList(new LambdaQueryWrapper<IeRecord>()
.eq(IeRecord::getUserId, userId)
.orderByDesc(IeRecord::getUpdateTime)
.orderByDesc(IeRecord::getCreateTime));
IPage<IeRecord> result = recordMapper.pageWithUnread(new Page<>(current, size), userId);
List<IeRecord> 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<IeRecord> records) {
if (records == null || records.isEmpty()) {
return;
}
IeUserProfile profile = findProfileByUserId(record.getTargetUserId());
if (profile == null || !isCompletedProfile(profile)) {
Set<Long> targetUserIds = new HashSet<>();
for (IeRecord record : records) {
if (record != null && record.getTargetUserId() != null) {
targetUserIds.add(record.getTargetUserId());
}
}
if (targetUserIds.isEmpty()) {
return;
}
List<IeUserProfile> profiles = userProfileMapper.selectList(new LambdaQueryWrapper<IeUserProfile>()
.in(IeUserProfile::getUserId, targetUserIds));
Map<Long, IeUserProfile> 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<IeRoomMessage>()
.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<IeRecord>()
.eq(IeRecord::getUserId, userId)
.eq(IeRecord::getRoomId, roomId)
.last("limit 1"));
return record == null ? null : record.getLastReadTime();
}
@Override
public IPage<IeReport> pageReports(Long userId, Integer pageNumber, Integer pageSize) {
Page<IeReport> page = new Page<>(safePageNumber(pageNumber), safePageSize(pageSize));

9
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<IeUserProfile>()
.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);

42
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("图片包含不适合展示的内容,请更换一张");

35
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<IeUserProfile>()
.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<String> normalizeImages(List<String> images) {
List<String> result = new ArrayList<>();
if (images == null) {

21
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<String, String> params) throws Exception {
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);

7
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);

92
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<WxMpTemplateData> 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<WxMpTemplateData> 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<Worker> workers, String message, String orderId) {
if (workers == null || workers.isEmpty()) {
return;
}
Set<String> 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)

17
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小时未处理自动默认同意
*

78
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<MallDeliveryOrderM
@Autowired
private SmsUtil smsUtil;
@Autowired
private ShopTakeawayMapper shopTakeawayMapper;
@Autowired(required = false)
private BillingRecordProducer billingRecordProducer;
@Autowired
private FreeOrderCacheUtil freeOrderCacheUtil;
@ -95,15 +94,9 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
@Autowired
private DeliveryRejectUtil deliveryRejectUtil;
@Autowired
WorkerRedisCacheUtil workerRedisCacheUtil;
@Autowired
WorkerOrderCacheUtil workerOrderCacheUtil;
@Autowired
private ShopService shopService;
@Autowired
private MallOrderGroupMapper mallOrderGroupMapper;
@ -119,12 +112,6 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
@Autowired
private WaitOrderCacheUtil waitOrderCacheUtil;
@Autowired
private RedisLuaScripts redisLuaScripts;
@Autowired
private ShopIndexCountCacheUtil shopIndexCountCacheUtil;
@Autowired
private MallOrderGoodsMapper mallOrderGoodsMapper;
@ -135,7 +122,7 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
private MerchantOrderSeqUtil merchantOrderSeqUtil;
@Autowired
private CommentService commentService;
private WorkerService workerService;
@Autowired
private WorkerServiceImpl workerServiceImpl;
@ -360,10 +347,12 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
List<MallOrderGoods> goodsList = delivery.getGoodsList() == null ? new ArrayList<>() : new ArrayList<>(delivery.getGoodsList());
// 提前查好 Shop,避免重复查询(后续推送复用)
Shop cachedShop = null;
String pushClientId = null;
Map<String, String> 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<String> pushOrderIds = new ArrayList<>();
@ -409,6 +398,10 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
userPendingOrderCacheUtil.update(orderInner.getUserId(), orderVO);
// 收集推送 orderId
pushOrderIds.add(orderId);
String clientId = resolveShopPushClientId(orderInner);
if (StringUtils.isNotBlank(clientId)) {
pushClientIdMap.put(orderId, clientId);
}
}
}
// 逐条 UPDATE(各订单 numberCode 不同,无法单条 IN 批量更新 numberCode)
@ -492,15 +485,21 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
// 抢单大厅缓存去掉
waitOrderCacheUtil.remove(delivery.getRegionId(), delivery.getId());
// 极光推送:异步执行,不阻塞主事务(clientId 已在上方查好)
final String finalPushClientId = pushClientId;
if (Boolean.TRUE.equals(notifyShop) && StringUtils.isNotBlank(finalPushClientId) && !pushOrderIds.isEmpty()) {
String firstOrderId = pushOrderIds.get(0);
// 极光推送:异步执行,不阻塞主事务
final Map<String, String> finalPushClientIdMap = new HashMap<>(pushClientIdMap);
final List<String> 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<MallDeliveryOrderM
return 3;
}
private String resolveShopPushClientId(MallOrder order) {
if (order == null || StringUtils.isBlank(order.getShopId())) {
return null;
}
Shop shop = getShopFromCache(order.getRegionId(), order.getShopId());
return shop == null ? null : shop.getClientId();
}
private String generateTransferPickupCode() {
return String.format("%04d", new Random().nextInt(10000));
}
@ -924,6 +931,8 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
}
//给用户发送短信通知被拒绝
smsUtil.sendCode(delivery.getReceiverPhone(), null, SettingConstant.SMS_TYPE.SMS_REJECT_ORDER.name());
recordSmsBilling(delivery.getRegionId(), SettingConstant.SMS_TYPE.SMS_REJECT_ORDER.name(),
delivery.getReceiverPhone(), "mall_delivery_order", delivery.getId());
return result;
}
}
@ -950,6 +959,12 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
return delivery;
}
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 assembleOrderGoodsForDeliveries(List<MallDeliveryOrder> records) {
if (records == null || records.isEmpty()) {
return;
@ -1290,6 +1305,12 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
throw new RuntimeException("重新指定配送员失败");
}
if(delivery.getWorkerId() != null){
Worker worker = workerService.findByWorkerId(delivery.getWorkerId());
if(worker.getClientId() != null){
jPushService.sendPushNotification(worker.getClientId(), "您有一笔新的指定配送单,请及时接单", order.getId());
}
}
delivery.setWorkerId(newWorkerId);
delivery.setWorkerName(newWorkerName);
@ -1297,6 +1318,7 @@ public class MallDeliveryOrderServiceImpl extends ServiceImpl<MallDeliveryOrderM
syncReassignedWorkerCache(oldWorkerId, newWorkerId, delivery);
sendReassignedWorkerWechatNotifyAfterCommit(delivery);
sendReassignedWorkerTimeoutMessage(delivery);
log.info("配送单 {} 重新指定配送员: workerId={}, workerName={}", deliveryId, newWorkerId, newWorkerName);
}

18
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderGroupServiceImpl.java

@ -6,6 +6,7 @@ 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.mall.billing.mq.BillingRecordProducer;
import cc.hiver.mall.controller.AdminSeckillGroupController;
import cc.hiver.mall.dao.mapper.*;
import cc.hiver.mall.entity.*;
@ -79,6 +80,8 @@ public class MallOrderGroupServiceImpl extends ServiceImpl<MallOrderGroupMapper,
@Autowired
private SmsUtil smsUtil;
@Autowired(required = false)
private BillingRecordProducer billingRecordProducer;
@Autowired
private MerchantOrderSeqUtil merchantOrderSeqUtil;
@ -239,6 +242,8 @@ public class MallOrderGroupServiceImpl extends ServiceImpl<MallOrderGroupMapper,
delivery.setWorkerPhone("");
delivery.setWorkerName("");
smsUtil.sendCode(delivery.getReceiverPhone(), null, SettingConstant.SMS_TYPE.SMS_WORKEROUT.name());
recordSmsBilling(delivery.getRegionId(), SettingConstant.SMS_TYPE.SMS_WORKEROUT.name(),
delivery.getReceiverPhone(), "mall_order_group", order.getId());
}
}
@ -410,6 +415,13 @@ public class MallOrderGroupServiceImpl extends ServiceImpl<MallOrderGroupMapper,
* 触发配送订单异步事件微信推给骑手加入超时检查死信队列等
*/
private void triggerDeliveryAsyncEvents(MallOrder order, MallDeliveryOrder delivery) {
/*if(!delivery.getWorkerId().isEmpty()){
Worker worker = workerService.findByWorkerId(delivery.getWorkerId());
if(!worker.getClientId().isEmpty()){
jPushService.sendPushNotification(worker.getClientId(), "您有一笔新的指定配送单,请及时接单", order.getId());
}
}*/
String orderId = order.getId();
// 1. 发送微信通知(交由MQ)
orderAsyncProducer.sendWechatNotify(orderId);
@ -523,4 +535,10 @@ public class MallOrderGroupServiceImpl extends ServiceImpl<MallOrderGroupMapper,
public List<MallOrderGroup> 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);
}
}
}

36
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<MallOrderMapper, MallOrder
@Autowired
private RedisLuaScripts redisLuaScripts;
@Autowired
private WorkerService workerService;
@Autowired
private MallDeliveryOrderService mallDeliveryOrderService;
@ -128,6 +134,8 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
@Autowired
private SmsUtil smsUtil;
@Autowired(required = false)
private BillingRecordProducer billingRecordProducer;
@Autowired
private MallDeliveryOrderMapper mallDeliveryOrderMapper;
@ -843,6 +851,12 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
* 触发配送订单异步事件微信推给骑手加入超时检查死信队列等
*/
private void triggerDeliveryAsyncEvents(MallOrder order, MallDeliveryOrder delivery) {
/*if(!delivery.getWorkerId().isEmpty()){
Worker worker = workerService.findByWorkerId(delivery.getWorkerId());
if(!worker.getClientId().isEmpty()){
jPushService.sendPushNotification(worker.getClientId(), "您有一笔新的指定配送单,请及时接单", order.getId());
}
}*/
String orderId = order.getId();
// 1. 发送微信通知(交由MQ)
orderAsyncProducer.sendWechatNotify(orderId);
@ -2651,7 +2665,15 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
if(order.getDeliveryType() == DELIVERY_TYPE_EXPRESS){
if(order.getOrderType() == ORDER_TYPE_FACETOFACE){
MallDeliveryOrder delivery = mallDeliveryOrderMapper.selectByGroupId(order.getId());
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){
@ -2662,7 +2684,15 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
LambdaQueryWrapper<MallDeliveryOrder> 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<MallOrderMapper, MallOrder
updateSaleCacheIncremental(shopId, regionId, items, productMap, shopSaleCount);
}
private void recordSmsBilling(String regionId, String smsType, String mobile, String bizType, String bizId) {
if (billingRecordProducer != null) {
billingRecordProducer.recordSms(regionId, smsType, mobile, bizType, bizId);
}
}
}

13
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallRefundRecordServiceImpl.java

@ -7,6 +7,7 @@ import cc.hiver.core.common.utils.SnowFlakeUtil;
import cc.hiver.core.entity.Worker;
import cc.hiver.core.serviceimpl.JPushServiceImpl;
import cc.hiver.core.serviceimpl.WorkerServiceImpl;
import cc.hiver.mall.billing.mq.BillingRecordProducer;
import cc.hiver.mall.dao.mapper.*;
import cc.hiver.mall.entity.*;
import cc.hiver.mall.pojo.query.MallRefundRecordPageQuery;
@ -68,6 +69,8 @@ public class MallRefundRecordServiceImpl extends ServiceImpl<MallRefundRecordMap
@Autowired
private SmsUtil smsUtil;
@Autowired(required = false)
private BillingRecordProducer billingRecordProducer;
@Autowired
private JPushServiceImpl jPushService;
@ -922,6 +925,8 @@ public class MallRefundRecordServiceImpl extends ServiceImpl<MallRefundRecordMap
try {
if (order.getReceiverPhone() != null && !"".equals(order.getReceiverPhone())) {
smsUtil.sendCode(order.getReceiverPhone(), null, SettingConstant.SMS_TYPE.SMS_REFUNDUSER.name());
recordSmsBilling(order.getRegionId(), SettingConstant.SMS_TYPE.SMS_REFUNDUSER.name(),
order.getReceiverPhone(), "mall_refund_record", mallRefundRecord.getId());
}
} catch (Exception ignored) {
}
@ -950,6 +955,8 @@ public class MallRefundRecordServiceImpl extends ServiceImpl<MallRefundRecordMap
: findDeliveryByOrderId(order.getId());
if (delivery != null && delivery.getWorkerPhone() != null && !"".equals(delivery.getWorkerPhone())) {
smsUtil.sendCode(delivery.getWorkerPhone(), null, SettingConstant.SMS_TYPE.SMS_REFUNDWORKER.name());
recordSmsBilling(delivery.getRegionId(), SettingConstant.SMS_TYPE.SMS_REFUNDWORKER.name(),
delivery.getWorkerPhone(), "mall_refund_record", mallRefundRecord.getId());
}
} catch (Exception ignored) {
}
@ -1027,4 +1034,10 @@ public class MallRefundRecordServiceImpl extends ServiceImpl<MallRefundRecordMap
}
return result;
}
private void recordSmsBilling(String regionId, String smsType, String mobile, String bizType, String bizId) {
if (billingRecordProducer != null) {
billingRecordProducer.recordSms(regionId, smsType, mobile, bizType, bizId);
}
}
}

21
hiver-modules/hiver-mall/src/main/resources/db/billing_usage_record.sql

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS billing_usage_record (
id VARCHAR(64) NOT NULL COMMENT '主键',
record_type VARCHAR(32) NOT NULL COMMENT '记录类型:sms/content_audit/real_name_auth',
region_id VARCHAR(64) NULL COMMENT '校区/学校ID,用于代理商费用归集',
billing_date DATE NOT NULL COMMENT '计费日期',
occur_time DATETIME NOT NULL COMMENT '业务实际发生时间',
provider VARCHAR(64) NULL COMMENT '服务商:sms/aliyun/aliyun_three_element',
usage_type VARCHAR(64) NULL COMMENT '调用类型:短信模板类型、审核类型等',
biz_type VARCHAR(64) NULL COMMENT '业务类型',
biz_id VARCHAR(64) NULL COMMENT '业务ID',
user_id VARCHAR(64) NULL COMMENT '用户ID',
receive_mobile VARCHAR(32) NULL COMMENT '短信接收手机号',
result_status VARCHAR(64) NULL COMMENT '调用结果状态',
request_id VARCHAR(128) NULL COMMENT '第三方请求ID/任务ID',
detail VARCHAR(500) NULL COMMENT '调用摘要,不保存实名敏感原文',
create_time DATETIME NOT NULL COMMENT '记录创建时间',
PRIMARY KEY (id),
INDEX idx_billing_region_date_type (region_id, billing_date, record_type, usage_type),
INDEX idx_billing_biz (biz_type, biz_id),
INDEX idx_billing_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方计费使用流水';

8
hiver-modules/hiver-mall/src/main/resources/db/ie_records_page_indexes.sql

@ -0,0 +1,8 @@
ALTER TABLE ie_record
ADD INDEX idx_ie_record_user_time (user_id, update_time, create_time);
ALTER TABLE ie_room_message
ADD INDEX idx_ie_room_message_unread (receiver_id, room_id, sender_id, is_blocked, create_time);
ALTER TABLE ie_user_profile
ADD INDEX idx_ie_user_profile_user (user_id);
Loading…
Cancel
Save