合并远程主账与后台更新:保留查价出单,并纳入企微通讯录同步与工单展示。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+47
-1
@@ -11,21 +11,28 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.modules.inquiry.dto.StaffWecomSyncResult;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
||||
import org.jeecg.modules.inquiry.service.IInquiryStaffRoleService;
|
||||
import org.jeecg.modules.inquiry.wecom.WeComAddressBookClient;
|
||||
import org.jeecg.modules.inquiry.wecom.WeComDepartment;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 员工岗位管理 API。
|
||||
* <p>
|
||||
* 路径前缀 /inquiry/staffRole;权限码 inquiry:staff:*。
|
||||
* 不做企微换号,wecom_id 由运营手工绑定。
|
||||
* wecom_id 可手工维护,也可经 /syncWecom 从企微通讯录 upsert(姓名/手机/部门)。
|
||||
* 浏览器禁止直连企微;同步只服务展示,不挡询价主路径。
|
||||
*/
|
||||
@Slf4j
|
||||
@Api(tags = "询价-员工岗位")
|
||||
@@ -36,6 +43,9 @@ public class InquiryStaffRoleController extends JeecgController<InquiryStaffRole
|
||||
@Autowired
|
||||
private IInquiryStaffRoleService staffRoleService;
|
||||
|
||||
@Autowired
|
||||
private WeComAddressBookClient weComAddressBookClient;
|
||||
|
||||
private static final Map<String, String> ROLE_LABEL = new HashMap<>();
|
||||
|
||||
static {
|
||||
@@ -122,6 +132,42 @@ public class InquiryStaffRoleController extends JeecgController<InquiryStaffRole
|
||||
return Result.OK(staffRoleService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 企微通讯录全量 upsert。
|
||||
* 权限:编辑人员即可同步;不自动删/停用企微侧消失的人。
|
||||
*/
|
||||
@AutoLog(value = "员工岗位-同步企微通讯录")
|
||||
@ApiOperation("同步企微通讯录")
|
||||
@RequiresPermissions("inquiry:staff:edit")
|
||||
@PostMapping("/syncWecom")
|
||||
public Result<StaffWecomSyncResult> syncWecom() {
|
||||
String operator = currentUsername();
|
||||
StaffWecomSyncResult result = staffRoleService.syncFromWeCom(operator);
|
||||
return Result.OK(result.getMessage(), result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 企微部门下拉选项。浏览器禁止直连企微,只打本接口。
|
||||
*/
|
||||
@ApiOperation("企微部门列表")
|
||||
@RequiresPermissions("inquiry:staff:list")
|
||||
@GetMapping("/wecomDepartments")
|
||||
public Result<List<WeComDepartment>> wecomDepartments() {
|
||||
return Result.OK(weComAddressBookClient.listDepartments());
|
||||
}
|
||||
|
||||
private String currentUsername() {
|
||||
try {
|
||||
Object principal = SecurityUtils.getSubject().getPrincipal();
|
||||
if (principal instanceof LoginUser) {
|
||||
return ((LoginUser) principal).getUsername();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 无登录主体时用系统标记
|
||||
}
|
||||
return "system";
|
||||
}
|
||||
|
||||
private void normalize(InquiryStaffRole body) {
|
||||
if (StringUtils.isBlank(body.getName()) || StringUtils.isBlank(body.getWecomId())) {
|
||||
throw new IllegalArgumentException("姓名与企微 userid 不能为空");
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package org.jeecg.modules.inquiry.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 企微通讯录同步结果(给后台 toast / 日志)。
|
||||
*/
|
||||
@Data
|
||||
@ApiModel("员工企微同步结果")
|
||||
public class StaffWecomSyncResult {
|
||||
|
||||
@ApiModelProperty("企微侧已激活成员数")
|
||||
private int fetched;
|
||||
|
||||
@ApiModelProperty("新建条数")
|
||||
private int created;
|
||||
|
||||
@ApiModelProperty("更新条数")
|
||||
private int updated;
|
||||
|
||||
@ApiModelProperty("跳过条数(无 userid 等)")
|
||||
private int skipped;
|
||||
|
||||
@ApiModelProperty("摘要文案")
|
||||
private String message;
|
||||
}
|
||||
+11
@@ -1,6 +1,7 @@
|
||||
package org.jeecg.modules.inquiry.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.inquiry.dto.StaffWecomSyncResult;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -19,4 +20,14 @@ public interface IInquiryStaffRoleService extends IService<InquiryStaffRole> {
|
||||
* 停用员工(status=disabled),不物理删除。
|
||||
*/
|
||||
void disable(String id);
|
||||
|
||||
/**
|
||||
* 从企微通讯录全量 upsert:按 wecom_id 更新姓名/手机/部门,缺失则新建(默认销售岗)。
|
||||
* <p>
|
||||
* 不删、不停用企微侧消失的人;不覆盖已有 roleCode / routes / 提醒开关。
|
||||
*
|
||||
* @param operator 操作人(写入 createBy/updateBy)
|
||||
* @return 同步计数
|
||||
*/
|
||||
StaffWecomSyncResult syncFromWeCom(String operator);
|
||||
}
|
||||
|
||||
+80
-2
@@ -2,9 +2,15 @@ package org.jeecg.modules.inquiry.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.inquiry.dto.StaffWecomSyncResult;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryStaffRoleMapper;
|
||||
import org.jeecg.modules.inquiry.service.IInquiryStaffRoleService;
|
||||
import org.jeecg.modules.inquiry.wecom.WeComAddressBookClient;
|
||||
import org.jeecg.modules.inquiry.wecom.WeComMember;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -14,15 +20,25 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 员工岗位 CRUD 实现。
|
||||
* 员工岗位 CRUD + 企微通讯录 upsert。
|
||||
* <p>
|
||||
* 线程:跟随 HTTP 请求线程;无全局锁。wecom_id 唯一约束由库保证。
|
||||
* 线程:跟随 HTTP 请求线程;同步方法串行化,避免并发双写同一 wecom_id。
|
||||
* 无全局工单大锁;wecom_id 唯一约束由库保证。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class InquiryStaffRoleServiceImpl extends ServiceImpl<InquiryStaffRoleMapper, InquiryStaffRole>
|
||||
implements IInquiryStaffRoleService {
|
||||
|
||||
private static final String[] ROLE_CODES = {"sales", "air", "sea", "land"};
|
||||
private static final String DEFAULT_ROLE = "sales";
|
||||
private static final String DEFAULT_ROLE_LABEL = "销售";
|
||||
|
||||
@Autowired
|
||||
private WeComAddressBookClient weComAddressBookClient;
|
||||
|
||||
/** 同步互斥:同一进程内禁止两路并发全量 upsert。 */
|
||||
private final Object syncLock = new Object();
|
||||
|
||||
@Override
|
||||
public Map<String, Long> countByRole() {
|
||||
@@ -54,4 +70,66 @@ public class InquiryStaffRoleServiceImpl extends ServiceImpl<InquiryStaffRoleMap
|
||||
public InquiryStaffRole findByWecomId(String wecomId) {
|
||||
return getOne(new QueryWrapper<InquiryStaffRole>().eq("wecom_id", wecomId).last("LIMIT 1"), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public StaffWecomSyncResult syncFromWeCom(String operator) {
|
||||
synchronized (syncLock) {
|
||||
List<WeComMember> members = weComAddressBookClient.listActiveMembers();
|
||||
int created = 0;
|
||||
int updated = 0;
|
||||
int skipped = 0;
|
||||
Date now = new Date();
|
||||
String op = StringUtils.defaultIfBlank(operator, "wecom-sync");
|
||||
|
||||
for (WeComMember m : members) {
|
||||
if (m == null || StringUtils.isBlank(m.getUserid())) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
InquiryStaffRole existing = findByWecomId(m.getUserid());
|
||||
if (existing == null) {
|
||||
InquiryStaffRole row = new InquiryStaffRole();
|
||||
row.setWecomId(m.getUserid());
|
||||
row.setName(StringUtils.defaultIfBlank(m.getName(), m.getUserid()));
|
||||
row.setMobile(StringUtils.trimToNull(m.getMobile()));
|
||||
row.setDepartName(StringUtils.trimToNull(m.getDepartmentName()));
|
||||
row.setRoleCode(DEFAULT_ROLE);
|
||||
row.setRoleLabel(DEFAULT_ROLE_LABEL);
|
||||
row.setNotifyEventException(0);
|
||||
row.setStatus("active");
|
||||
row.setCreateBy(op);
|
||||
row.setCreateTime(now);
|
||||
row.setUpdateBy(op);
|
||||
row.setUpdateTime(now);
|
||||
save(row);
|
||||
created++;
|
||||
} else {
|
||||
// 只刷新展示字段;岗位/线路/提醒开关保留运营手工配置
|
||||
existing.setName(StringUtils.defaultIfBlank(m.getName(), existing.getName()));
|
||||
if (StringUtils.isNotBlank(m.getMobile())) {
|
||||
existing.setMobile(m.getMobile());
|
||||
}
|
||||
if (StringUtils.isNotBlank(m.getDepartmentName())) {
|
||||
existing.setDepartName(m.getDepartmentName());
|
||||
}
|
||||
existing.setUpdateBy(op);
|
||||
existing.setUpdateTime(now);
|
||||
updateById(existing);
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
|
||||
StaffWecomSyncResult result = new StaffWecomSyncResult();
|
||||
result.setFetched(members.size());
|
||||
result.setCreated(created);
|
||||
result.setUpdated(updated);
|
||||
result.setSkipped(skipped);
|
||||
result.setMessage(String.format(
|
||||
"同步完成:企微成员 %d,新建 %d,更新 %d,跳过 %d",
|
||||
members.size(), created, updated, skipped));
|
||||
log.info("企微通讯录同步 operator={} {}", op, result.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+59
-1
@@ -12,14 +12,17 @@ import org.jeecg.modules.inquiry.dto.TicketTransitionRequest;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryAttachment;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryExceptionCase;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryQuote;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryStaffRole;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicket;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicketStatusLog;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryAttachmentMapper;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryExceptionCaseMapper;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryQuoteMapper;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryStaffRoleMapper;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryTicketMapper;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryTicketStatusLogMapper;
|
||||
import org.jeecg.modules.inquiry.service.IInquiryWorkOrderService;
|
||||
import org.jeecg.modules.inquiry.support.InquiryTicketDisplaySupport;
|
||||
import org.jeecg.modules.inquiry.tms.InquiryTmsQueryService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -29,16 +32,21 @@ import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 询价工单主账实现。
|
||||
* <p>
|
||||
* 副作用:改 inq_ticket.status、写 inq_ticket_status_log、改 inq_exception_case。
|
||||
* 列表/详情会就地补展示字段(salesName、demandTitle/Detail),只影响响应,不写回库。
|
||||
* 线程:单请求事务;只锁当前工单行(DB 行级),不串行其它工单。
|
||||
* 为何:Graph/前端成功不能冒充主账六态成功,所有 transition 必须过本服务。
|
||||
*/
|
||||
@@ -56,6 +64,8 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
|
||||
private InquiryExceptionCaseMapper exceptionCaseMapper;
|
||||
@Autowired
|
||||
private InquiryTmsQueryService tmsQueryService;
|
||||
@Autowired
|
||||
private InquiryStaffRoleMapper staffRoleMapper;
|
||||
|
||||
@Override
|
||||
public IPage<InquiryTicket> pageView(Page<InquiryTicket> page, Map<String, String> filters) {
|
||||
@@ -64,6 +74,7 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
|
||||
if (StringUtils.isNotBlank(keyword)) {
|
||||
qw.and(w -> w.like(InquiryTicket::getWorkOrderNo, keyword)
|
||||
.or().like(InquiryTicket::getSalesName, keyword)
|
||||
.or().like(InquiryTicket::getSalesWecomId, keyword)
|
||||
.or().like(InquiryTicket::getDemandTitle, keyword)
|
||||
.or().like(InquiryTicket::getOrigin, keyword)
|
||||
.or().like(InquiryTicket::getDestination, keyword));
|
||||
@@ -100,7 +111,9 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
|
||||
qw.le(InquiryTicket::getCreateTime, dateTo + " 23:59:59");
|
||||
}
|
||||
qw.orderByDesc(InquiryTicket::getCreateTime);
|
||||
return page(page, qw);
|
||||
IPage<InquiryTicket> result = page(page, qw);
|
||||
enrichDisplayFields(result.getRecords());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -109,6 +122,7 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
|
||||
if (ticket == null) {
|
||||
throw new IllegalArgumentException("工单不存在: " + workOrderNo);
|
||||
}
|
||||
enrichDisplayFields(Collections.singletonList(ticket));
|
||||
List<InquiryQuote> quotes = quoteMapper.selectList(new LambdaQueryWrapper<InquiryQuote>()
|
||||
.eq(InquiryQuote::getWorkOrderNo, workOrderNo)
|
||||
.orderByAsc(InquiryQuote::getCreateTime));
|
||||
@@ -131,6 +145,50 @@ public class InquiryWorkOrderServiceImpl extends ServiceImpl<InquiryTicketMapper
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表/详情响应补齐:发起人姓名(员工表)+ 询价需求摘要(结构化字段)。
|
||||
* 副作用:只改内存中的 ticket 展示字段,不 updateById。
|
||||
*/
|
||||
private void enrichDisplayFields(List<InquiryTicket> tickets) {
|
||||
if (tickets == null || tickets.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Map<String, String> staffNameByWecom = loadStaffNames(tickets);
|
||||
for (InquiryTicket ticket : tickets) {
|
||||
if (ticket == null) {
|
||||
continue;
|
||||
}
|
||||
String staffName = staffNameByWecom.get(StringUtils.trimToEmpty(ticket.getSalesWecomId()));
|
||||
ticket.setSalesName(InquiryTicketDisplaySupport.resolveSalesDisplayName(
|
||||
ticket.getSalesName(), ticket.getSalesWecomId(), staffName));
|
||||
InquiryTicketDisplaySupport.applyRequirementSummary(ticket);
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量按企微 userid 取员工姓名,避免列表 N+1。 */
|
||||
private Map<String, String> loadStaffNames(List<InquiryTicket> tickets) {
|
||||
Set<String> wecomIds = new HashSet<>();
|
||||
for (InquiryTicket ticket : tickets) {
|
||||
if (ticket != null && StringUtils.isNotBlank(ticket.getSalesWecomId())) {
|
||||
wecomIds.add(ticket.getSalesWecomId().trim());
|
||||
}
|
||||
}
|
||||
if (wecomIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<InquiryStaffRole> staffRows = staffRoleMapper.selectList(new LambdaQueryWrapper<InquiryStaffRole>()
|
||||
.in(InquiryStaffRole::getWecomId, new ArrayList<>(wecomIds))
|
||||
.eq(InquiryStaffRole::getStatus, "active"));
|
||||
Map<String, String> result = new HashMap<>();
|
||||
for (InquiryStaffRole row : staffRows) {
|
||||
if (row == null || StringUtils.isBlank(row.getWecomId()) || StringUtils.isBlank(row.getName())) {
|
||||
continue;
|
||||
}
|
||||
result.put(row.getWecomId().trim(), row.getName().trim());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public InquiryTicket transition(TicketTransitionRequest req, String operator) {
|
||||
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
package org.jeecg.modules.inquiry.support;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicket;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 工单列表/详情的展示口径,对齐现网 InquiryAggregateService.requirementSummaryLines。
|
||||
* <p>
|
||||
* 职责:把结构化字段拼成「发起人可读名 + 询价需求多行摘要」,禁止把 FP:AIR|… 原文甩到后台。
|
||||
* 副作用:无写库;仅生成展示用字符串。
|
||||
* 为何:inq_ticket.sales_name / demand_* 可能为空或被写入管道原文,后台必须以员工表与字段快照为准。
|
||||
*/
|
||||
public final class InquiryTicketDisplaySupport {
|
||||
|
||||
private InquiryTicketDisplaySupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 询价需求多行摘要:首行标题(业务线 · 路线),后续为货量/品名。
|
||||
* 与旧后台 requirementSummaryLines 同口径,便于对照验收。
|
||||
*/
|
||||
public static List<String> requirementSummaryLines(InquiryTicket ticket) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
if (ticket == null) {
|
||||
return lines;
|
||||
}
|
||||
String businessLine = upper(ticket.getBusinessLine());
|
||||
|
||||
List<String> heading = new ArrayList<>();
|
||||
String lineLabel = firstText(ticket.getBizTypeLabel(), businessLineLabel(ticket.getBusinessLine()));
|
||||
if (StringUtils.isNotBlank(lineLabel)) {
|
||||
heading.add(lineLabel);
|
||||
}
|
||||
String route = route(ticket.getOrigin(), ticket.getDestination());
|
||||
if (StringUtils.isNotBlank(route)) {
|
||||
heading.add(route);
|
||||
}
|
||||
addLine(lines, join(heading, " · "));
|
||||
|
||||
List<String> quantities = new ArrayList<>();
|
||||
if ("AIR".equals(businessLine) || containsChinese(lineLabel, "空运")) {
|
||||
addLabel(quantities, "件数", ticket.getPieces());
|
||||
addUnit(quantities, "毛重", ticket.getWeightKg(), "kg");
|
||||
addUnit(quantities, "体积", ticket.getVolumeCbm(), "CBM");
|
||||
} else if ("SEA".equals(businessLine) || containsChinese(lineLabel, "海运")) {
|
||||
addLabel(quantities, "件数", ticket.getPieces());
|
||||
addUnit(quantities, "重量", ticket.getWeightKg(), "kg");
|
||||
addUnit(quantities, "体积", ticket.getVolumeCbm(), "CBM");
|
||||
} else if ("LAND".equals(businessLine) || containsChinese(lineLabel, "陆运")) {
|
||||
addUnit(quantities, "体积", ticket.getVolumeCbm(), "CBM");
|
||||
addUnit(quantities, "重量", ticket.getWeightKg(), "kg");
|
||||
addLabel(quantities, "件数", ticket.getPieces());
|
||||
} else {
|
||||
addLabel(quantities, "件数", ticket.getPieces());
|
||||
addUnit(quantities, "重量", ticket.getWeightKg(), "kg");
|
||||
addUnit(quantities, "体积", ticket.getVolumeCbm(), "CBM");
|
||||
}
|
||||
addLine(lines, join(quantities, " · "));
|
||||
|
||||
List<String> cargo = new ArrayList<>();
|
||||
boolean land = "LAND".equals(businessLine) || containsChinese(lineLabel, "陆运");
|
||||
addLabel(cargo, land ? "货物品名" : "品名", ticket.getGoodsName());
|
||||
if ("AIR".equals(businessLine) || containsChinese(lineLabel, "空运")) {
|
||||
addLabel(cargo, "客户", ticket.getCustomerName());
|
||||
}
|
||||
addLine(lines, join(cargo, " · "));
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把摘要写回 ticket 的 demandTitle/demandDetail,仅用于 API 响应,不落库。
|
||||
*/
|
||||
public static void applyRequirementSummary(InquiryTicket ticket) {
|
||||
if (ticket == null) {
|
||||
return;
|
||||
}
|
||||
List<String> lines = requirementSummaryLines(ticket);
|
||||
if (lines.isEmpty()) {
|
||||
// 结构化字段全空时,仍禁止把 FP: 管道原文当需求展示
|
||||
if (looksLikeRawFp(ticket.getDemandTitle())) {
|
||||
ticket.setDemandTitle(null);
|
||||
}
|
||||
if (looksLikeRawFp(ticket.getDemandDetail())) {
|
||||
ticket.setDemandDetail(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ticket.setDemandTitle(lines.get(0));
|
||||
if (lines.size() == 1) {
|
||||
ticket.setDemandDetail("");
|
||||
} else {
|
||||
ticket.setDemandDetail(join(lines.subList(1, lines.size()), "\n"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起人展示名:优先已有中文名,其次员工表姓名;企微 userid 不能冒充人名。
|
||||
*/
|
||||
public static String resolveSalesDisplayName(String salesName, String salesWecomId, String staffName) {
|
||||
if (StringUtils.isNotBlank(salesName) && !looksLikeWecomUserId(salesName)) {
|
||||
return salesName.trim();
|
||||
}
|
||||
if (StringUtils.isNotBlank(staffName)) {
|
||||
return staffName.trim();
|
||||
}
|
||||
if (StringUtils.isNotBlank(salesName)) {
|
||||
return salesName.trim();
|
||||
}
|
||||
return StringUtils.trimToEmpty(salesWecomId);
|
||||
}
|
||||
|
||||
public static boolean looksLikeRawFp(String value) {
|
||||
String raw = StringUtils.trimToEmpty(value);
|
||||
return raw.startsWith("FP:") || raw.contains("|") && raw.contains(":");
|
||||
}
|
||||
|
||||
/** 纯拼音/企微 userid(无中文)不当作「发起人名称」。 */
|
||||
public static boolean looksLikeWecomUserId(String value) {
|
||||
String raw = StringUtils.trimToEmpty(value);
|
||||
if (raw.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if (raw.chars().anyMatch(ch -> Character.UnicodeScript.of(ch) == Character.UnicodeScript.HAN)) {
|
||||
return false;
|
||||
}
|
||||
return raw.matches("^[A-Za-z][A-Za-z0-9._-]{1,64}$");
|
||||
}
|
||||
|
||||
private static String businessLineLabel(String value) {
|
||||
if ("AIR".equalsIgnoreCase(value)) {
|
||||
return "空运";
|
||||
}
|
||||
if ("SEA".equalsIgnoreCase(value)) {
|
||||
return "海运";
|
||||
}
|
||||
if ("LAND".equalsIgnoreCase(value)) {
|
||||
return "陆运";
|
||||
}
|
||||
if ("MULTI".equalsIgnoreCase(value)) {
|
||||
return "多段联运";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String route(String origin, String destination) {
|
||||
if (StringUtils.isBlank(origin) && StringUtils.isBlank(destination)) {
|
||||
return null;
|
||||
}
|
||||
return (StringUtils.isNotBlank(origin) ? origin.trim() : "-")
|
||||
+ " → "
|
||||
+ (StringUtils.isNotBlank(destination) ? destination.trim() : "-");
|
||||
}
|
||||
|
||||
private static void addLine(List<String> lines, String value) {
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
lines.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addLabel(List<String> values, String label, Object value) {
|
||||
if (value == null || !StringUtils.isNotBlank(String.valueOf(value))) {
|
||||
return;
|
||||
}
|
||||
values.add(label + " " + plain(value));
|
||||
}
|
||||
|
||||
private static void addUnit(List<String> values, String label, Object value, String unit) {
|
||||
if (value == null || !StringUtils.isNotBlank(String.valueOf(value))) {
|
||||
return;
|
||||
}
|
||||
values.add(label + " " + plain(value) + unit);
|
||||
}
|
||||
|
||||
private static String plain(Object value) {
|
||||
if (value instanceof BigDecimal) {
|
||||
return ((BigDecimal) value).stripTrailingZeros().toPlainString();
|
||||
}
|
||||
return String.valueOf(value).trim();
|
||||
}
|
||||
|
||||
private static String join(List<String> values, String sep) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String v : values) {
|
||||
if (!StringUtils.isNotBlank(v)) {
|
||||
continue;
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
sb.append(sep);
|
||||
}
|
||||
sb.append(v.trim());
|
||||
}
|
||||
return sb.length() == 0 ? null : sb.toString();
|
||||
}
|
||||
|
||||
private static String firstText(String... values) {
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
for (String v : values) {
|
||||
if (StringUtils.isNotBlank(v)) {
|
||||
return v.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String upper(String value) {
|
||||
return value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static boolean containsChinese(String value, String token) {
|
||||
return StringUtils.isNotBlank(value) && value.contains(token);
|
||||
}
|
||||
}
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
package org.jeecg.modules.inquiry.wecom;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 企微通讯录只读客户端(主账侧)。
|
||||
* <p>
|
||||
* 职责:gettoken + department/list + user/list,给员工页同步姓名/手机/部门。
|
||||
* 密钥:环境变量 WECOM_CORP_ID / WECOM_SECRET(与询价应用 1000010 同一套,见 prompt/03)。
|
||||
* 本机开发可设 WECOM_SOCKS_HOST/PORT,经测试机 SOCKS 出口,避免本机公网 IP 不在企微白名单(errcode=60020)。
|
||||
* 禁止:浏览器直连;禁止在同步失败时挡询价主路径;禁止把 Secret 写入日志。
|
||||
* 线程:HTTP 请求线程调用;token 缓存带锁;connect/read 超时,禁止无限阻塞。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class WeComAddressBookClient {
|
||||
|
||||
private static final String DEFAULT_API_BASE = "https://qyapi.weixin.qq.com";
|
||||
/** 根部门 id;fetch_child=1 拉全树成员。 */
|
||||
private static final int ROOT_DEPARTMENT_ID = 1;
|
||||
|
||||
@Value("${WECOM_CORP_ID:}")
|
||||
private String corpId;
|
||||
|
||||
@Value("${WECOM_SECRET:}")
|
||||
private String secret;
|
||||
|
||||
@Value("${WECOM_API_BASE:" + DEFAULT_API_BASE + "}")
|
||||
private String apiBase;
|
||||
|
||||
/** 可选:本机开发经 SOCKS 出站(如 127.0.0.1,配合 ssh -D)。 */
|
||||
@Value("${WECOM_SOCKS_HOST:}")
|
||||
private String socksHost;
|
||||
|
||||
@Value("${WECOM_SOCKS_PORT:1080}")
|
||||
private int socksPort;
|
||||
|
||||
private RestTemplate restTemplate;
|
||||
private final Object tokenLock = new Object();
|
||||
private String cachedToken;
|
||||
private long tokenExpireAtMs;
|
||||
|
||||
@PostConstruct
|
||||
public void initHttpClient() {
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(5_000);
|
||||
factory.setReadTimeout(15_000);
|
||||
if (StringUtils.isNotBlank(socksHost)) {
|
||||
factory.setProxy(new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(socksHost.trim(), socksPort)));
|
||||
log.info("企微 HTTP 经 SOCKS {}:{} 出站(绕过本机 IP 白名单限制)", socksHost, socksPort);
|
||||
}
|
||||
RestTemplate rt = new RestTemplate(factory);
|
||||
rt.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
|
||||
this.restTemplate = rt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置是否齐全(不访问外网)。
|
||||
*/
|
||||
public boolean isConfigured() {
|
||||
return StringUtils.isNotBlank(corpId) && StringUtils.isNotBlank(secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取应用可见范围内的已激活成员(去重 userid)。
|
||||
* <p>
|
||||
* 副作用:出站 HTTP 调企微;失败抛 IllegalStateException,由上层转 Result。
|
||||
*/
|
||||
public List<WeComMember> listActiveMembers() {
|
||||
if (!isConfigured()) {
|
||||
throw new IllegalStateException("未配置 WECOM_CORP_ID / WECOM_SECRET,无法同步通讯录");
|
||||
}
|
||||
String token = getAccessToken();
|
||||
Map<Integer, String> deptNames = loadDepartmentNames(token);
|
||||
String url = UriComponentsBuilder
|
||||
.fromHttpUrl(trimBase(apiBase) + "/cgi-bin/user/list")
|
||||
.queryParam("access_token", token)
|
||||
.queryParam("department_id", ROOT_DEPARTMENT_ID)
|
||||
.queryParam("fetch_child", 1)
|
||||
.toUriString();
|
||||
JSONObject body = getJson(url);
|
||||
assertOk(body, "user/list");
|
||||
JSONArray userlist = body.getJSONArray("userlist");
|
||||
Map<String, WeComMember> byUserid = new LinkedHashMap<>();
|
||||
if (userlist == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
for (int i = 0; i < userlist.size(); i++) {
|
||||
JSONObject u = userlist.getJSONObject(i);
|
||||
if (u == null) {
|
||||
continue;
|
||||
}
|
||||
String userid = StringUtils.trimToEmpty(u.getString("userid"));
|
||||
if (userid.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
int status = u.getIntValue("status");
|
||||
// 1=已激活;其它状态不同步进业务表,避免半离职账号入主账
|
||||
if (status != 1) {
|
||||
continue;
|
||||
}
|
||||
String name = StringUtils.defaultIfBlank(u.getString("name"), userid);
|
||||
String mobile = StringUtils.trimToEmpty(u.getString("mobile"));
|
||||
String deptName = resolveDepartmentName(u.getJSONArray("department"), deptNames);
|
||||
byUserid.put(userid, new WeComMember(userid, name, mobile, deptName, status));
|
||||
}
|
||||
return new ArrayList<>(byUserid.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取企微部门树(扁平列表,带 path 便于下拉展示)。
|
||||
* <p>
|
||||
* 副作用:出站 HTTP;失败抛 IllegalStateException。
|
||||
*/
|
||||
public List<WeComDepartment> listDepartments() {
|
||||
if (!isConfigured()) {
|
||||
throw new IllegalStateException("未配置 WECOM_CORP_ID / WECOM_SECRET,无法拉取部门");
|
||||
}
|
||||
String token = getAccessToken();
|
||||
String url = UriComponentsBuilder
|
||||
.fromHttpUrl(trimBase(apiBase) + "/cgi-bin/department/list")
|
||||
.queryParam("access_token", token)
|
||||
.toUriString();
|
||||
JSONObject body = getJson(url);
|
||||
assertOk(body, "department/list");
|
||||
JSONArray list = body.getJSONArray("department");
|
||||
if (list == null || list.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
Map<Integer, JSONObject> byId = new LinkedHashMap<>();
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
JSONObject d = list.getJSONObject(i);
|
||||
if (d != null) {
|
||||
byId.put(d.getIntValue("id"), d);
|
||||
}
|
||||
}
|
||||
|
||||
List<WeComDepartment> result = new ArrayList<>();
|
||||
for (JSONObject d : byId.values()) {
|
||||
int id = d.getIntValue("id");
|
||||
String name = StringUtils.defaultIfBlank(d.getString("name"), String.valueOf(id));
|
||||
int parentId = d.getIntValue("parentid");
|
||||
String path = buildDepartmentPath(id, byId);
|
||||
result.add(new WeComDepartment(id, name, parentId, path));
|
||||
}
|
||||
// 按路径排序,下拉更易扫
|
||||
result.sort((a, b) -> StringUtils.defaultString(a.getPath()).compareTo(StringUtils.defaultString(b.getPath())));
|
||||
return result;
|
||||
}
|
||||
|
||||
private String buildDepartmentPath(int id, Map<Integer, JSONObject> byId) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
int cursor = id;
|
||||
// 防环:最多 32 层
|
||||
for (int depth = 0; depth < 32; depth++) {
|
||||
JSONObject d = byId.get(cursor);
|
||||
if (d == null) {
|
||||
break;
|
||||
}
|
||||
parts.add(0, StringUtils.defaultIfBlank(d.getString("name"), String.valueOf(cursor)));
|
||||
int parentId = d.getIntValue("parentid");
|
||||
if (parentId <= 0 || parentId == cursor || !byId.containsKey(parentId)) {
|
||||
break;
|
||||
}
|
||||
cursor = parentId;
|
||||
}
|
||||
return String.join(" / ", parts);
|
||||
}
|
||||
|
||||
private Map<Integer, String> loadDepartmentNames(String token) {
|
||||
String url = UriComponentsBuilder
|
||||
.fromHttpUrl(trimBase(apiBase) + "/cgi-bin/department/list")
|
||||
.queryParam("access_token", token)
|
||||
.toUriString();
|
||||
JSONObject body = getJson(url);
|
||||
assertOk(body, "department/list");
|
||||
Map<Integer, String> map = new HashMap<>();
|
||||
JSONArray list = body.getJSONArray("department");
|
||||
if (list == null) {
|
||||
return map;
|
||||
}
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
JSONObject d = list.getJSONObject(i);
|
||||
if (d == null) {
|
||||
continue;
|
||||
}
|
||||
map.put(d.getIntValue("id"), StringUtils.defaultString(d.getString("name")));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private String resolveDepartmentName(JSONArray departmentIds, Map<Integer, String> deptNames) {
|
||||
if (departmentIds == null || departmentIds.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
List<String> names = new ArrayList<>();
|
||||
for (int i = 0; i < departmentIds.size(); i++) {
|
||||
int id = departmentIds.getIntValue(i);
|
||||
String n = deptNames.get(id);
|
||||
if (StringUtils.isNotBlank(n)) {
|
||||
names.add(n);
|
||||
}
|
||||
}
|
||||
return String.join("/", names);
|
||||
}
|
||||
|
||||
private String getAccessToken() {
|
||||
synchronized (tokenLock) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (StringUtils.isNotBlank(cachedToken) && now < tokenExpireAtMs) {
|
||||
return cachedToken;
|
||||
}
|
||||
String url = UriComponentsBuilder
|
||||
.fromHttpUrl(trimBase(apiBase) + "/cgi-bin/gettoken")
|
||||
.queryParam("corpid", corpId)
|
||||
.queryParam("corpsecret", secret)
|
||||
.toUriString();
|
||||
JSONObject body = getJson(url);
|
||||
assertOk(body, "gettoken");
|
||||
String token = body.getString("access_token");
|
||||
if (StringUtils.isBlank(token)) {
|
||||
throw new IllegalStateException("企微 gettoken 未返回 access_token");
|
||||
}
|
||||
int expiresIn = body.getIntValue("expires_in");
|
||||
if (expiresIn <= 0) {
|
||||
expiresIn = 7200;
|
||||
}
|
||||
// 提前 5 分钟失效,避免边界过期
|
||||
cachedToken = token;
|
||||
tokenExpireAtMs = now + Math.max(60, expiresIn - 300) * 1000L;
|
||||
log.info("企微 access_token 已刷新,expiresIn={}s", expiresIn);
|
||||
return cachedToken;
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject getJson(String url) {
|
||||
try {
|
||||
String raw = restTemplate.getForObject(url, String.class);
|
||||
if (StringUtils.isBlank(raw)) {
|
||||
throw new IllegalStateException("企微接口返回空响应");
|
||||
}
|
||||
return JSONObject.parseObject(raw);
|
||||
} catch (IllegalStateException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("调用企微失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertOk(JSONObject body, String api) {
|
||||
if (body == null) {
|
||||
throw new IllegalStateException("企微 " + api + " 响应为空");
|
||||
}
|
||||
int errcode = body.getIntValue("errcode");
|
||||
if (errcode != 0) {
|
||||
String errmsg = body.getString("errmsg");
|
||||
throw new IllegalStateException("企微 " + api + " 失败 errcode=" + errcode + " errmsg=" + errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
private static String trimBase(String base) {
|
||||
if (StringUtils.isBlank(base)) {
|
||||
return DEFAULT_API_BASE;
|
||||
}
|
||||
return base.endsWith("/") ? base.substring(0, base.length() - 1) : base;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package org.jeecg.modules.inquiry.wecom;
|
||||
|
||||
/**
|
||||
* 企微部门投影,供员工表单下拉。
|
||||
*/
|
||||
public class WeComDepartment {
|
||||
|
||||
private final int id;
|
||||
private final String name;
|
||||
private final int parentId;
|
||||
/** 自根到本节点的展示路径,如「友通达 / 销售部」。 */
|
||||
private final String path;
|
||||
|
||||
public WeComDepartment(int id, String name, int parentId, String path) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.parentId = parentId;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getParentId() {
|
||||
return parentId;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package org.jeecg.modules.inquiry.wecom;
|
||||
|
||||
/**
|
||||
* 企微通讯录成员投影(仅姓名展示所需字段)。
|
||||
* <p>
|
||||
* 来源:qyapi user/list;不做岗位推断,岗位仍由运营在后台维护。
|
||||
*/
|
||||
public class WeComMember {
|
||||
|
||||
private final String userid;
|
||||
private final String name;
|
||||
private final String mobile;
|
||||
private final String departmentName;
|
||||
private final int status;
|
||||
|
||||
public WeComMember(String userid, String name, String mobile, String departmentName, int status) {
|
||||
this.userid = userid;
|
||||
this.name = name;
|
||||
this.mobile = mobile;
|
||||
this.departmentName = departmentName;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getUserid() {
|
||||
return userid;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getMobile() {
|
||||
return mobile;
|
||||
}
|
||||
|
||||
public String getDepartmentName() {
|
||||
return departmentName;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/** 企微 status=1 表示已激活,可入员工表。 */
|
||||
public boolean isActive() {
|
||||
return status == 1;
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package org.jeecg.modules.inquiry.support;
|
||||
|
||||
import org.jeecg.modules.inquiry.entity.InquiryTicket;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 工单列表展示口径:发起人名称 + 询价需求摘要,对齐旧后台。
|
||||
*/
|
||||
public class InquiryTicketDisplaySupportTest {
|
||||
|
||||
@Test
|
||||
public void airRequirementSummaryMatchesLegacyShape() {
|
||||
InquiryTicket ticket = new InquiryTicket();
|
||||
ticket.setBusinessLine("AIR");
|
||||
ticket.setBizTypeLabel("空运");
|
||||
ticket.setOrigin("ZUH");
|
||||
ticket.setDestination("CRK");
|
||||
ticket.setPieces(10);
|
||||
ticket.setWeightKg(new BigDecimal("1"));
|
||||
ticket.setVolumeCbm(new BigDecimal("8"));
|
||||
ticket.setGoodsName("茶叶");
|
||||
ticket.setDemandDetail("FP:AIR|ZUH|CRK|茶叶|10件|1KGS|8CBM|2026-09-11");
|
||||
|
||||
List<String> lines = InquiryTicketDisplaySupport.requirementSummaryLines(ticket);
|
||||
assertEquals("空运 · ZUH → CRK", lines.get(0));
|
||||
assertEquals("件数 10 · 毛重 1kg · 体积 8CBM", lines.get(1));
|
||||
assertEquals("品名 茶叶", lines.get(2));
|
||||
|
||||
InquiryTicketDisplaySupport.applyRequirementSummary(ticket);
|
||||
assertEquals("空运 · ZUH → CRK", ticket.getDemandTitle());
|
||||
assertEquals("件数 10 · 毛重 1kg · 体积 8CBM\n品名 茶叶", ticket.getDemandDetail());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void landUsesGoodsNameLabel() {
|
||||
InquiryTicket ticket = new InquiryTicket();
|
||||
ticket.setBusinessLine("LAND");
|
||||
ticket.setBizTypeLabel("陆运");
|
||||
ticket.setOrigin("上海");
|
||||
ticket.setDestination("北宁Bac Ninh");
|
||||
ticket.setGoodsName("设备");
|
||||
|
||||
List<String> lines = InquiryTicketDisplaySupport.requirementSummaryLines(ticket);
|
||||
assertEquals("陆运 · 上海 → 北宁Bac Ninh", lines.get(0));
|
||||
assertEquals("货物品名 设备", lines.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void salesNamePrefersStaffChineseName() {
|
||||
assertEquals("吴吉林",
|
||||
InquiryTicketDisplaySupport.resolveSalesDisplayName(null, "WuJiLin", "吴吉林"));
|
||||
assertEquals("陈柏成",
|
||||
InquiryTicketDisplaySupport.resolveSalesDisplayName("WuJiLin", "WuJiLin", "陈柏成"));
|
||||
assertFalse(InquiryTicketDisplaySupport.looksLikeWecomUserId("陈柏成"));
|
||||
assertTrue(InquiryTicketDisplaySupport.looksLikeWecomUserId("WuJiLin"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user