169 changed files with 3432 additions and 4386 deletions
-33
View File
@@ -133,38 +133,6 @@
</dependencies> </dependencies>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.nutz</groupId> <groupId>org.nutz</groupId>
<artifactId>nutzboot-parent</artifactId> <artifactId>nutzboot-parent</artifactId>
@@ -183,7 +151,6 @@
<easypoi.version>4.5.0</easypoi.version> <easypoi.version>4.5.0</easypoi.version>
<jsoup.veision>1.15.3</jsoup.veision> <jsoup.veision>1.15.3</jsoup.veision>
<mysql-connector-java.version>8.0.28</mysql-connector-java.version> <mysql-connector-java.version>8.0.28</mysql-connector-java.version>
<tomcat.version>9.0.121</tomcat.version>
<sa-token.version>1.25.0</sa-token.version> <sa-token.version>1.25.0</sa-token.version>
<easy-captcha.version>1.6.2</easy-captcha.version> <easy-captcha.version>1.6.2</easy-captcha.version>
<emoji-java.veision>4.0.0</emoji-java.veision> <emoji-java.veision>4.0.0</emoji-java.veision>
+1 -50
View File
@@ -7,7 +7,6 @@
<properties> <properties>
<nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version> <nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version>
<nutz.version>1.r.70-SNAPSHOT</nutz.version> <nutz.version>1.r.70-SNAPSHOT</nutz.version>
<tomcat.version>9.0.121</tomcat.version>
<mysql-connector-java.version>8.0.28</mysql-connector-java.version> <mysql-connector-java.version>8.0.28</mysql-connector-java.version>
<jaxb-api.version>2.3.1</jaxb-api.version> <jaxb-api.version>2.3.1</jaxb-api.version>
<slf4j.version>2.0.8</slf4j.version> <slf4j.version>2.0.8</slf4j.version>
@@ -145,22 +144,8 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.nutz</groupId> <groupId>org.nutz</groupId>
<artifactId>nutzboot-starter-tomcat</artifactId> <artifactId>nutzboot-starter-jetty</artifactId>
<version>${nutzboot.version}</version> <version>${nutzboot.version}</version>
<exclusions>
<exclusion>
<groupId>org.gandon.tomcat</groupId>
<artifactId>juli-to-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutz-plugins-websocket</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.nutz</groupId> <groupId>org.nutz</groupId>
@@ -439,40 +424,6 @@
</dependencies> </dependencies>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<!-- Servlet API 由 Tomcat 提供,防止旧 API 混入可执行 JAR。 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<!-- 显式覆盖导入的 NutzBoot BOM,避免回退到其默认 Tomcat 8.5。 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.nutz</groupId> <groupId>org.nutz</groupId>
<artifactId>nutzboot-parent</artifactId> <artifactId>nutzboot-parent</artifactId>
@@ -11,7 +11,6 @@ import com.budwk.app.web.commons.auth.satoken.SaTokenDaoRedisImpl;
import com.budwk.app.web.commons.auth.satoken.StpInterfaceImpl; import com.budwk.app.web.commons.auth.satoken.StpInterfaceImpl;
import com.budwk.app.web.commons.base.Globals; import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.web.commons.ext.pubsub.WebPubSub; import com.budwk.app.web.commons.ext.pubsub.WebPubSub;
import com.budwk.app.web.commons.ext.handler.WkTomcatConfiguration;
import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService; import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.beetl.core.GroupTemplate; import org.beetl.core.GroupTemplate;
@@ -66,7 +65,6 @@ public class MainLauncher {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
NbApp nb = new NbApp().setArgs(args).setPrintProcDoc(true); NbApp nb = new NbApp().setArgs(args).setPrintProcDoc(true);
nb.getAppContext().setMainPackage("com.budwk"); nb.getAppContext().setMainPackage("com.budwk");
nb.addListener(new WkTomcatConfiguration());
nb.run(); nb.run();
} }
@@ -93,15 +93,6 @@ public class FlowTodoCenterController {
return Result.success(pagination); return Result.success(pagination);
} }
/**
* 分页查询当前用户的已办任务,仅返回列表展示及详情跳转所需的信息。
*
* @param pageNumber 查询页码
* @param pageSize 每页记录数
* @param searchKeyword 流程名称查询关键字
* @param category 流程分类
* @return 已办任务分页结果,不包含完整流程变量和任务表单数据
*/
@At @At
@SaCheckLogin @SaCheckLogin
public Result done(Integer pageNumber, Integer pageSize, String searchKeyword, String category) { public Result done(Integer pageNumber, Integer pageSize, String searchKeyword, String category) {
@@ -115,13 +106,8 @@ public class FlowTodoCenterController {
t.h5FormKey, t.h5FormKey,
t.createdAt, t.createdAt,
t.finishTime, t.finishTime,
-- 保留任务变量字段类型,但已办列表不返回其中的业务表单数据。 t.variable AS taskVariable,
'{}' AS taskVariable, ins.variable,
-- 列表仅需流程名称和申请人,避免完整变量中的身份证等信息泄露。
JSON_OBJECT(
'instanceName', ins.variable -> '$.instanceName',
'initiatorName', ins.variable -> '$.initiatorName'
) AS variable,
ins.state AS instanceState, ins.state AS instanceState,
ins.id AS instanceId, ins.id AS instanceId,
ins.businessNo, ins.businessNo,
@@ -23,7 +23,6 @@ import com.budwk.app.flow.entity.ProcessCcInstance;
import com.budwk.app.flow.entity.ProcessDefine; import com.budwk.app.flow.entity.ProcessDefine;
import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.entity.ProcessTaskActor;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum; import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
import com.budwk.app.flow.enums.ProcessSubmitTypeEnum; import com.budwk.app.flow.enums.ProcessSubmitTypeEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum; import com.budwk.app.flow.enums.ProcessTaskStateEnum;
@@ -557,18 +556,11 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
public void deleteProcessInstanceById(Long processInstanceId) { public void deleteProcessInstanceById(Long processInstanceId) {
List<ProcessTask> taskList = dao().query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId)); List<ProcessTask> taskList = dao().query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId));
List<Long> taskIds = taskList.stream().map(ProcessTask::getId).toList(); List<Long> taskIds = taskList.stream().map(ProcessTask::getId).toList();
// 任务参与人依赖流程任务,删除流程时先清理参与人,避免遗留无法回查所属流程的孤立记录。
if (CollectionUtil.isNotEmpty(taskIds)) {
dao().clear(ProcessTaskActor.class, Cnd.where(ProcessTaskActor::getProcessTaskId, "in", taskIds));
dao().clear(ProcessTask.class, Cnd.where(ProcessTask::getId, "in", taskIds)); dao().clear(ProcessTask.class, Cnd.where(ProcessTask::getId, "in", taskIds));
}
// 抄送记录直接依赖流程实例,需要与实例一并清理。
dao().clear(ProcessCcInstance.class, Cnd.where(ProcessCcInstance::getProcessInstanceId, "=", processInstanceId));
dao().clear(ProcessInstance.class, Cnd.where(ProcessInstance::getId, "=", processInstanceId)); dao().clear(ProcessInstance.class, Cnd.where(ProcessInstance::getId, "=", processInstanceId));
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public void deleteProcessInstanceByBusinessKey(String businessKey) { public void deleteProcessInstanceByBusinessKey(String businessKey) {
ProcessInstance processInstance = fetch(Cnd.where(ProcessInstance::getBusinessNo, "=", businessKey)); ProcessInstance processInstance = fetch(Cnd.where(ProcessInstance::getBusinessNo, "=", businessKey));
if (processInstance != null) { if (processInstance != null) {
@@ -411,6 +411,15 @@ public class Sys_user extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 50) @ColDefine(type = ColType.VARCHAR, width = 50)
private String openId; private String openId;
@Column("virtual_id")
@Comment("虚拟用户ID")
@ColDefine(type = ColType.VARCHAR, width = 64)
private String virtualId;
@Column("virtual_org_id")
@Comment("虚拟组织ID")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String virtualOrgId;
/** /**
* 基金会员 * 基金会员
@@ -25,6 +25,8 @@ public interface SysFileService extends BaseService<Sys_file> {
*/ */
String uploadReturnUrl(String engine, TempFile file); String uploadReturnUrl(String engine, TempFile file);
String uploadReturnUrl(String engine, String fileName, byte[] bytes);
/** /**
* 分页 * 分页
*/ */
@@ -33,10 +33,6 @@ import org.nutz.mvc.upload.TempFile;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -47,9 +43,6 @@ import java.nio.file.StandardOpenOption;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Iterator;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -61,9 +54,6 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
static String IMG_BASE64_PATTERN = "<img\\s+[^>]*src\\s*=\\s*['\"](data:image/[^'\"]+;base64,[^'\"]+)['\"][^>]*>"; static String IMG_BASE64_PATTERN = "<img\\s+[^>]*src\\s*=\\s*['\"](data:image/[^'\"]+;base64,[^'\"]+)['\"][^>]*>";
private static final int DOWNLOAD_FILE_CACHE_LIMIT = 2000; private static final int DOWNLOAD_FILE_CACHE_LIMIT = 2000;
private final Map<String, Sys_file> downloadFileCache = new ConcurrentHashMap<>(); private final Map<String, Sys_file> downloadFileCache = new ConcurrentHashMap<>();
private static final Set<String> UPLOAD_SUFFIXES = Set.of("gif", "jpg", "jpeg", "png", "doc", "docx",
"xls", "xlsx", "rar", "zip", "7z", "txt", "pdf", "pptx", "ppt", "mp4");
private static final Set<String> ACTIVE_SUFFIXES = Set.of("svg", "svgz", "html", "htm", "xhtml", "xml", "js", "mjs", "swf");
public SysFileServiceImpl(Dao dao) { public SysFileServiceImpl(Dao dao) {
super(dao); super(dao);
@@ -79,6 +69,11 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
return this.storageFile(engine, file, false); return this.storageFile(engine, file, false);
} }
@Override
public String uploadReturnUrl(String engine, String fileName, byte[] bytes) {
return this.storageFile(engine, fileName, bytes, false);
}
@Override @Override
public Pagination page(Sys_file file) { public Pagination page(Sys_file file) {
return null; return null;
@@ -91,13 +86,11 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
@Override @Override
public void download(String id, HttpServletRequest request, HttpServletResponse response) throws IOException { public void download(String id, HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setHeader("X-Content-Type-Options", "nosniff"); if (handleCachedFileRequest(id, request, response)) {
response.setHeader("Cache-Control", "private, no-store"); return;
}
Sys_file sys_file = fetchDownloadFile(id); Sys_file sys_file = fetchDownloadFile(id);
// 历史主动内容也必须在缓存协商前拦截,避免旧 SVG 继续在业务域下执行。 if (!ObjectUtil.isEmpty(sys_file) && handleCachedImageRequest(sys_file, request, response)) {
if (sys_file != null && (ACTIVE_SUFFIXES.contains(StrUtil.blankToDefault(sys_file.getSuffix(), "").toLowerCase(Locale.ROOT))
|| ACTIVE_SUFFIXES.contains(FileUtil.extName(sys_file.getName()).toLowerCase(Locale.ROOT)))) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "不允许访问此类型的附件");
return; return;
} }
if (ObjectUtil.isEmpty(sys_file)) { if (ObjectUtil.isEmpty(sys_file)) {
@@ -124,6 +117,16 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
} }
} }
private boolean handleCachedFileRequest(String id, HttpServletRequest request, HttpServletResponse response) {
String etag = buildFileEtag(id);
if (etag.equals(request.getHeader("If-None-Match"))) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
setImageCacheHeaders(response, etag);
return true;
}
return false;
}
private Sys_file fetchDownloadFile(String id) { private Sys_file fetchDownloadFile(String id) {
if (StrUtil.isBlank(id)) { if (StrUtil.isBlank(id)) {
return null; return null;
@@ -142,15 +145,23 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
return sysFile; return sysFile;
} }
private boolean writeImageResponse(Sys_file sysFile, byte[] bytes, HttpServletResponse response) throws IOException { private boolean handleCachedImageRequest(Sys_file sysFile, HttpServletRequest request, HttpServletResponse response) {
if (!isImage(sysFile)) { if (!isImage(sysFile)) {
return false; return false;
} }
String suffix = StrUtil.blankToDefault(sysFile.getSuffix(), FileUtil.extName(sysFile.getName())).toLowerCase(Locale.ROOT); String etag = buildFileEtag(sysFile);
if (!isValidImage(bytes, suffix)) { if (etag.equals(request.getHeader("If-None-Match"))) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "图片内容与文件类型不符"); response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
setImageCacheHeaders(response, etag);
return true; return true;
} }
return false;
}
private boolean writeImageResponse(Sys_file sysFile, byte[] bytes, HttpServletResponse response) throws IOException {
if (!isImage(sysFile)) {
return false;
}
setImageCacheHeaders(response, buildFileEtag(sysFile)); setImageCacheHeaders(response, buildFileEtag(sysFile));
response.setHeader("Content-Disposition", "inline;filename=" + java.net.URLEncoder.encode(sysFile.getName(), java.nio.charset.StandardCharsets.UTF_8)); response.setHeader("Content-Disposition", "inline;filename=" + java.net.URLEncoder.encode(sysFile.getName(), java.nio.charset.StandardCharsets.UTF_8));
response.setHeader("Content-Length", String.valueOf(bytes.length)); response.setHeader("Content-Length", String.valueOf(bytes.length));
@@ -160,9 +171,9 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
} }
private void setImageCacheHeaders(HttpServletResponse response, String etag) { private void setImageCacheHeaders(HttpServletResponse response, String etag) {
response.setHeader("Cache-Control", "private, no-cache"); response.setHeader("Cache-Control", "public, max-age=604800, immutable");
response.setHeader("ETag", etag); response.setHeader("ETag", etag);
response.setDateHeader("Expires", 0); response.setDateHeader("Expires", System.currentTimeMillis() + 604800000L);
} }
private String buildFileEtag(Sys_file sysFile) { private String buildFileEtag(Sys_file sysFile) {
@@ -170,12 +181,12 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
} }
private String buildFileEtag(String id) { private String buildFileEtag(String id) {
return "\"safe-" + id + "\""; return "\"" + id + "\"";
} }
private boolean isImage(Sys_file sysFile) { private boolean isImage(Sys_file sysFile) {
String suffix = StrUtil.blankToDefault(sysFile.getSuffix(), FileUtil.extName(sysFile.getName())).toLowerCase(); String suffix = StrUtil.blankToDefault(sysFile.getSuffix(), FileUtil.extName(sysFile.getName())).toLowerCase();
return "jpg".equals(suffix) || "jpeg".equals(suffix) || "png".equals(suffix) || "gif".equals(suffix) || "webp".equals(suffix) || "bmp".equals(suffix); return "jpg".equals(suffix) || "jpeg".equals(suffix) || "png".equals(suffix) || "gif".equals(suffix) || "webp".equals(suffix) || "bmp".equals(suffix) || "svg".equals(suffix);
} }
private String getImageContentType(Sys_file sysFile) { private String getImageContentType(Sys_file sysFile) {
@@ -186,6 +197,7 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
case "gif" -> "image/gif"; case "gif" -> "image/gif";
case "webp" -> "image/webp"; case "webp" -> "image/webp";
case "bmp" -> "image/bmp"; case "bmp" -> "image/bmp";
case "svg" -> "image/svg+xml";
default -> "application/octet-stream"; default -> "application/octet-stream";
}; };
} }
@@ -298,7 +310,6 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
* 存储文件 * 存储文件
**/ **/
private String storageFile(String engine, TempFile file, boolean returnFileId) { private String storageFile(String engine, TempFile file, boolean returnFileId) {
validateUpload(file);
// 如果引擎为空,默认使用本地 // 如果引擎为空,默认使用本地
if (ObjectUtil.isEmpty(engine)) { if (ObjectUtil.isEmpty(engine)) {
engine = SysFileEngineTypeEnum.LOCAL.getValue(); engine = SysFileEngineTypeEnum.LOCAL.getValue();
@@ -372,53 +383,6 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
} }
} }
/** 所有业务上传共用校验,客户端声明的 MIME 类型不能作为图片真实性依据。 */
private void validateUpload(TempFile file) {
if (file == null || StrUtil.isBlank(file.getSubmittedFileName())) {
throw new BaseException("请选择要上传的文件");
}
String suffix = FileUtil.extName(file.getSubmittedFileName()).toLowerCase(Locale.ROOT);
if (!UPLOAD_SUFFIXES.contains(suffix)) {
throw new BaseException("不允许上传此类型的文件");
}
if (Set.of("gif", "jpg", "jpeg", "png").contains(suffix)) {
try {
if (!isValidImage(Files.readAllBytes(file.getFile().toPath()), suffix)) {
throw new BaseException("图片内容与文件类型不符");
}
} catch (IOException e) {
throw new BaseException("图片读取失败");
}
}
}
/** 解码真实图片并限制像素数量,阻止伪装为图片的 SVG 和异常大尺寸图片。 */
private boolean isValidImage(byte[] bytes, String suffix) {
try (ImageInputStream input = ImageIO.createImageInputStream(new ByteArrayInputStream(bytes))) {
if (input == null) {
return false;
}
Iterator<ImageReader> readers = ImageIO.getImageReaders(input);
if (!readers.hasNext()) {
return false;
}
ImageReader reader = readers.next();
try {
reader.setInput(input, true, true);
String expected = "jpg".equals(suffix) ? "jpeg" : suffix;
if (!expected.equalsIgnoreCase(reader.getFormatName())) {
return false;
}
long pixels = (long) reader.getWidth(0) * reader.getHeight(0);
return pixels > 0 && pixels <= 40000000 && reader.read(0) != null;
} finally {
reader.dispose();
}
} catch (IOException | RuntimeException e) {
return false;
}
}
/** /**
* 存储文件 * 存储文件
*/ */
@@ -10,9 +10,6 @@ import cn.dev33.satoken.servlet.model.SaStorageForServlet;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.Mvcs; import org.nutz.mvc.Mvcs;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponseWrapper;
/** /**
* @author wizzer@qq.com * @author wizzer@qq.com
*/ */
@@ -25,20 +22,7 @@ public class SaTokenContextImpl implements SaTokenContext {
@Override @Override
public SaResponse getResponse() { public SaResponse getResponse() {
// 当前 Sa-Token 版本没有 Cookie 安全属性配置,在写入登录 Cookie 时统一补齐。 return new SaResponseForServlet(Mvcs.getResp());
return new SaResponseForServlet(new HttpServletResponseWrapper(Mvcs.getResp()) {
/**
* 限制登录 Cookie 的脚本访问,并在 HTTPS 请求中仅允许安全传输。
*
* @param cookie Sa-Token 写入或清除的 Cookie
*/
@Override
public void addCookie(Cookie cookie) {
cookie.setHttpOnly(true);
cookie.setSecure(cookie.getSecure() || Mvcs.getReq().isSecure());
super.addCookie(cookie);
}
});
} }
@Override @Override
@@ -15,10 +15,6 @@ import java.lang.reflect.Method;
public class SaCheckLoginInterceptor implements MethodInterceptor { public class SaCheckLoginInterceptor implements MethodInterceptor {
@Override @Override
public void filter(InterceptorChain chain) throws Throwable { public void filter(InterceptorChain chain) throws Throwable {
if (SaTokenAuthIgnoreUtil.isIgnore()) {
chain.doChain();
return;
}
Method method = chain.getCallingMethod(); Method method = chain.getCallingMethod();
SaCheckLogin at = method.getAnnotation(SaCheckLogin.class); SaCheckLogin at = method.getAnnotation(SaCheckLogin.class);
SaManager.getStpLogic(at.type()).checkByAnnotation(at); SaManager.getStpLogic(at.type()).checkByAnnotation(at);
@@ -15,10 +15,6 @@ import java.lang.reflect.Method;
public class SaCheckPermissionInterceptor implements MethodInterceptor { public class SaCheckPermissionInterceptor implements MethodInterceptor {
@Override @Override
public void filter(InterceptorChain chain) throws Throwable { public void filter(InterceptorChain chain) throws Throwable {
if (SaTokenAuthIgnoreUtil.isIgnore()) {
chain.doChain();
return;
}
Method method = chain.getCallingMethod(); Method method = chain.getCallingMethod();
SaCheckPermission at = method.getAnnotation(SaCheckPermission.class); SaCheckPermission at = method.getAnnotation(SaCheckPermission.class);
SaManager.getStpLogic(at.type()).checkByAnnotation(at); SaManager.getStpLogic(at.type()).checkByAnnotation(at);
@@ -15,10 +15,6 @@ import java.lang.reflect.Method;
public class SaCheckRoleInterceptor implements MethodInterceptor { public class SaCheckRoleInterceptor implements MethodInterceptor {
@Override @Override
public void filter(InterceptorChain chain) throws Throwable { public void filter(InterceptorChain chain) throws Throwable {
if (SaTokenAuthIgnoreUtil.isIgnore()) {
chain.doChain();
return;
}
Method method = chain.getCallingMethod(); Method method = chain.getCallingMethod();
SaCheckRole at = method.getAnnotation(SaCheckRole.class); SaCheckRole at = method.getAnnotation(SaCheckRole.class);
SaManager.getStpLogic(at.type()).checkByAnnotation(at); SaManager.getStpLogic(at.type()).checkByAnnotation(at);
@@ -1,60 +0,0 @@
package com.budwk.app.web.commons.auth.satoken.aop;
import com.budwk.app.web.commons.base.Globals;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.mvc.Mvcs;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.regex.Pattern;
/**
* Sa-Token 注解认证白名单工具。
* <p>
* 用于集中维护不需要登录、权限、角色校验即可访问的 URL,匹配规则与旧 Shiro 体系中的
* authIgnoreUrlArr 保持一致:数组中的每一项都按正则表达式与当前请求 URI 完整匹配。
*/
public class SaTokenAuthIgnoreUtil {
/**
* 不需要认证授权的 URL。
* <p>
* 参数说明:数组中的字符串为请求 URI 正则表达式,匹配的是去除项目 contextPath 后的路径。
* 返回值说明:通过 {@link #isIgnore()} 返回 booleantrue 表示当前请求跳过 Sa-Token 注解校验。
*/
private static final String[] AUTH_IGNORE_URL_ARR = Lang.array("/",
"/platform/activity/basic/scope/getScopeUser",
"/platform/fitnessWalk/stepManage/getActivityDateStep",
"/platform/fitnessWalk/stepManage/winningRecord",
"/platform/fitnessWalk/stepManage/updateStepMonth",
"/platform/fitnessWalk/stepRanking/getUserStepRanking",
"/platform/fitnessWalk/stepManage/getActivityQualifyProgressBar",
"/platform/fitnessWalk/stepWining/getUserStepWining",
"/platform/fitnessWalk/common/getIssueById",
"/platform/fitnessWalk/common/saveAnswer",
"/platform/fitnessWalk/stepWining/prizeOption",
"/platform/fitnessWalk/stepWining/prizeUsers",
"/platform/fitnessWalk/punchLottery/judgeWinningToPunch",
"/platform/fitnessWalk/punchLottery/getLotteryRecord",
"/platform/fitnessWalk/punchLottery/doExchange",
"/platform/fitnessWalk/punchLottery/getPunchWining",
"/platform/fitnessWalk/punchLottery/doReadLottery",
"/platform/fitnessWalk/activityRelation/isCurrentUserRelationActivityQualified",
"/platform/fitnessWalk/welfare/addWelfareList",
"/platform/fitnessWalk/weapp/.*");
/**
* 判断当前请求是否命中匿名访问白名单。
*
* @return boolean true 表示当前 URL 不需要执行登录、权限、角色注解校验;false 表示继续走原有 Sa-Token 校验逻辑
*/
public static boolean isIgnore() {
HttpServletRequest request = Mvcs.getReq();
if (request == null) {
return false;
}
String requestURI = Strings.sNull(request.getRequestURI()).replaceFirst("^" + Pattern.quote(Strings.sNull(Globals.AppBase)), "");
return Arrays.stream(AUTH_IGNORE_URL_ARR).anyMatch(v -> Pattern.compile(v).matcher(requestURI).matches());
}
}
@@ -1,6 +1,7 @@
package com.budwk.app.web.commons.ext.handler; package com.budwk.app.web.commons.ext.handler;
import org.nutz.boot.starter.WebServletFace; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.servlet.ErrorPageErrorHandler;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json; import org.nutz.json.Json;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
@@ -8,63 +9,28 @@ import org.nutz.log.Log;
import org.nutz.log.Logs; import org.nutz.log.Logs;
import javax.servlet.RequestDispatcher; import javax.servlet.RequestDispatcher;
import javax.servlet.DispatcherType;
import javax.servlet.Servlet;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServlet;
import java.io.IOException; import java.io.IOException;
/** /**
* 错误页拦截器,登陆后台显示友好提示 * 错误页拦截器,登陆后台显示友好提示
*/ */
@IocBean @IocBean
public class WkErrorPageHandler extends HttpServlet implements WebServletFace { public class WkErrorPageHandler extends ErrorPageErrorHandler {
private static final Log log = Logs.get(); private static final Log log = Logs.get();
/**
* 返回容器错误处理 Servlet 的注册名称。
* @return 独立于业务路由的 Servlet 名称
*/
@Override @Override
public String getName() { public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
return "wkErrorPage";
}
/**
* 返回容器错误页的内部派发地址。
* @return 错误页映射路径
*/
@Override
public String getPathSpec() {
return "/__container_error";
}
/**
* 将当前错误处理实例交给 NutzBoot 注册。
* @return 当前 Servlet 实例
*/
@Override
public Servlet getServlet() {
return this;
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
if (request.getDispatcherType() != DispatcherType.ERROR) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (response.getStatus() == 403 || response.getStatus() == 404 || response.getStatus() == 500) { if (response.getStatus() == 403 || response.getStatus() == 404 || response.getStatus() == 500) {
try { try {
if (isAjax(request)) { if (isAjax(request)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(Json.toJson(new NutMap("code", "-1").setv("msg", response.getStatus() + " error"))); response.getWriter().write(Json.toJson(new NutMap("code", "-1").setv("msg", response.getStatus() + " error")));
return; return;
} else { } else {
request.setAttribute("original_request_uri", request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI)); request.setAttribute("original_request_uri", request.getRequestURI());
RequestDispatcher rd = request.getRequestDispatcher("/platform/home/" + response.getStatus()); RequestDispatcher rd = request.getRequestDispatcher("/platform/home/" + response.getStatus());
rd.forward(request, response); rd.forward(request, response);
return; return;
@@ -73,7 +39,7 @@ public class WkErrorPageHandler extends HttpServlet implements WebServletFace {
log.error(e); log.error(e);
} }
} }
// 不回落到容器默认错误页,避免泄露异常堆栈和容器信息。 super.handle(target, baseRequest, request, response);
} }
private boolean isAjax(ServletRequest req) { private boolean isAjax(ServletRequest req) {
@@ -1,88 +0,0 @@
package com.budwk.app.web.commons.ext.handler;
import com.budwk.app.web.commons.ext.websocket.WkWebSocket;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.Executor;
import org.apache.catalina.core.StandardThreadExecutor;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.valves.ErrorReportValve;
import org.apache.tomcat.websocket.server.WsSci;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.nutz.boot.NbApp;
import org.nutz.boot.starter.tomcat.TomcatStarter;
import org.nutz.boot.tools.NbAppEventListener;
import org.nutz.ioc.impl.PropertiesProxy;
import java.util.Set;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
/**
* 在容器启动前补齐 NutzBoot Tomcat 的 WebSocket 注册和线程池绑定。
*/
public class WkTomcatConfiguration implements NbAppEventListener {
/**
* 配置官方嵌入式 Tomcat,保留项目的监听地址与消息推送能力。
*
* @param app 当前 NutzBoot 应用
* @param eventType 只在服务器启动前配置,避免重复注册端点
*/
@Override
public void whenStartServers(NbApp app, EventType eventType) {
if (eventType != EventType.before) {
return;
}
TomcatStarter starter = app.getAppContext().getIoc().get(TomcatStarter.class);
Tomcat tomcat = starter.getServer();
PropertiesProxy conf = app.getAppContext().getConf();
tomcat.getConnector().setProperty("address", starter.getHost());
// Starter 创建了共享线程池,但尚未把它交给 Connector 使用。
for (Executor executor : tomcat.getService().findExecutors()) {
if (executor instanceof StandardThreadExecutor) {
StandardThreadExecutor threads = (StandardThreadExecutor) executor;
threads.setMinSpareThreads(conf.getInt("tomcat.executor.minSpareThreads", 10));
threads.setMaxIdleTime(conf.getInt("tomcat.executor.maxIdleTime", 60000));
tomcat.getConnector().getProtocolHandler().setExecutor(threads);
break;
}
}
// NutzBoot 使用编程式 Context,必须显式运行官方 WebSocket 初始化器。
StandardContext context = (StandardContext) tomcat.getHost().findChild(starter.getContextPath());
context.setUseHttpOnly(conf.getBoolean("tomcat.session.cookie.httponly", true));
context.addServletContainerInitializer(new WsSci(), Set.of(WkWebSocket.class));
// Tomcat 的错误页再次 forward 时仍保持 ERROR 派发;让 MVC 渲染原业务错误页。
// Nutz 的过滤器在前一个监听器中注册,须等其完成后才能添加映射。
context.addApplicationLifecycleListener(new ServletContextListener() {
/**
* 在 Nutz 注册完成后补充业务错误页的 ERROR 派发映射。
* @param event 当前 Servlet 上下文初始化事件
*/
@Override
public void contextInitialized(ServletContextEvent event) {
FilterMap errorPages = new FilterMap();
errorPages.setFilterName("nutz");
errorPages.setDispatcher("ERROR");
// ERROR 再转发时 URL 匹配路径不变,按目标 Servlet 匹配才能进入 MVC。
errorPages.addServletName("default");
context.addFilterMap(errorPages);
}
});
// Host 的兜底错误页也不能重新暴露此前已关闭的版本和堆栈。
ErrorReportValve errorReport = null;
for (org.apache.catalina.Valve valve : tomcat.getHost().getPipeline().getValves()) {
if (valve instanceof ErrorReportValve) {
errorReport = (ErrorReportValve) valve;
break;
}
}
if (errorReport == null) {
errorReport = new ErrorReportValve();
tomcat.getHost().getPipeline().addValve(errorReport);
}
errorReport.setShowReport(false);
errorReport.setShowServerInfo(false);
}
}
@@ -162,10 +162,27 @@ public class ActivityWorksCollectionManageController {
@SaCheckPermission(value = {"activity.workscollection.manage", "activity.workscollection.new"}, mode = SaMode.OR) @SaCheckPermission(value = {"activity.workscollection.manage", "activity.workscollection.new"}, mode = SaMode.OR)
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public Result update(@Param("data") @Valid Activity_works_collection worksCollection) { public Result update(@Param("data") @Valid Activity_works_collection worksCollection) {
String validateMessage = activityWorksCollectionService.updatePreservingTypeIds(worksCollection); String validateMessage = activityWorksCollectionService.validateReferencedTypesBeforeUpdate(worksCollection);
if (StrUtil.isNotBlank(validateMessage)) { if (StrUtil.isNotBlank(validateMessage)) {
return Result.error(validateMessage); return Result.error(validateMessage);
} }
dao.update(worksCollection);
dao.updateLinks(worksCollection, "subjectTypes");
dao.insertLinks(worksCollection, "subjectTypes");
//清除页面已经删除的
dao.clear(Activity_works_subjectType.class,
Cnd.where(Activity_works_subjectType::getActivityId, "=", worksCollection.getId())
.andEX("id", "not in", worksCollection.getSubjectTypes().stream().map(Activity_works_subjectType::getId).toList()));
worksCollection.getSubjectTypes().forEach(item -> {
dao.updateLinks(item, "worksTypes");
dao.insertLinks(item, "worksTypes");
//清除页面已经删除的
List<String> workIds = item.getWorksTypes().stream().map(Activity_works_worksType::getId).toList();
dao.clear(Activity_works_worksType.class,
Cnd.where(Activity_works_worksType::getId, "not in ", workIds)
.and(Activity_works_worksType::getSubjectId,"=",item.getId()));
});
syncHomeActivity(worksCollection); syncHomeActivity(worksCollection);
return Result.success(); return Result.success();
} }
@@ -35,14 +35,6 @@ public interface ActivityWorksCollectionService extends BaseService<Activity_wor
*/ */
void cancelTemplate(String id); void cancelTemplate(String id);
/**
* 更新作品征集活动及其主题、作品类型,并保留所有原有类型ID。
*
* @param worksCollection 前端提交的活动及其主题、作品类型配置
* @return 更新成功返回 {@code null};数据层级异常或删除了投稿引用类型时返回提示信息
*/
String updatePreservingTypeIds(Activity_works_collection worksCollection);
/** /**
* 校验编辑活动时删除的主题类型、作品类型是否已被投稿引用。 * 校验编辑活动时删除的主题类型、作品类型是否已被投稿引用。
* *
@@ -21,9 +21,7 @@ import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -118,136 +116,6 @@ public class ActivityWorksCollectionServiceImpl extends BaseServiceImpl<Activity
Cnd.where("id", "=", id)); Cnd.where("id", "=", id));
} }
/**
* 活动编辑采用显式新增、更新、删除,避免通用关联保存重新生成已有类型ID。
* 原有主题和作品类型必须携带数据库ID;只有页面新增的类型允许生成新ID。
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public String updatePreservingTypeIds(Activity_works_collection worksCollection) {
if (worksCollection == null || StrUtil.isBlank(worksCollection.getId())) {
return "活动数据不存在,请刷新页面后重试";
}
Activity_works_collection persistedActivity = dao.fetch(Activity_works_collection.class, worksCollection.getId());
if (persistedActivity == null) {
return "活动不存在或已被删除";
}
List<Activity_works_subjectType> submittedSubjectTypes = CollUtil.defaultIfEmpty(
worksCollection.getSubjectTypes(), Collections.emptyList());
List<Activity_works_subjectType> persistedSubjectTypes = dao.query(Activity_works_subjectType.class,
Cnd.where(Activity_works_subjectType::getActivityId, "=", worksCollection.getId()));
Map<String, Activity_works_subjectType> persistedSubjectMap = persistedSubjectTypes.stream()
.collect(Collectors.toMap(Activity_works_subjectType::getId, item -> item));
Set<String> submittedSubjectIds = new HashSet<>();
for (Activity_works_subjectType subjectType : submittedSubjectTypes) {
if (subjectType == null) {
return "主题类型数据不能为空";
}
if (StrUtil.isNotBlank(subjectType.getId())) {
if (!persistedSubjectMap.containsKey(subjectType.getId())) {
return "主题类型数据已发生变化,请刷新页面后重新编辑";
}
if (!submittedSubjectIds.add(subjectType.getId())) {
return "主题类型数据重复,请刷新页面后重新编辑";
}
}
List<Activity_works_worksType> submittedWorksTypes = CollUtil.defaultIfEmpty(
subjectType.getWorksTypes(), Collections.emptyList());
if (StrUtil.isBlank(subjectType.getId())) {
boolean containsPersistedWorksType = submittedWorksTypes.stream()
.anyMatch(worksType -> worksType != null && StrUtil.isNotBlank(worksType.getId()));
if (containsPersistedWorksType) {
return "新增主题不能关联原有作品类型,请刷新页面后重新编辑";
}
continue;
}
List<Activity_works_worksType> persistedWorksTypes = dao.query(Activity_works_worksType.class,
Cnd.where(Activity_works_worksType::getSubjectId, "=", subjectType.getId()));
Map<String, Activity_works_worksType> persistedWorksTypeMap = persistedWorksTypes.stream()
.collect(Collectors.toMap(Activity_works_worksType::getId, item -> item));
Set<String> submittedWorksIds = new HashSet<>();
for (Activity_works_worksType worksType : submittedWorksTypes) {
if (worksType == null) {
return "作品类型数据不能为空";
}
if (StrUtil.isNotBlank(worksType.getId()) && !persistedWorksTypeMap.containsKey(worksType.getId())) {
return "作品类型数据已发生变化,请刷新页面后重新编辑";
}
if (StrUtil.isNotBlank(worksType.getId()) && !submittedWorksIds.add(worksType.getId())) {
return "作品类型数据重复,请刷新页面后重新编辑";
}
}
}
String validateMessage = validateReferencedTypesBeforeUpdate(worksCollection);
if (StrUtil.isNotBlank(validateMessage)) {
return validateMessage;
}
dao.update(worksCollection);
for (Activity_works_subjectType subjectType : submittedSubjectTypes) {
subjectType.setActivityId(worksCollection.getId());
if (StrUtil.isBlank(subjectType.getId())) {
dao.insert(subjectType);
} else {
dao.update(subjectType);
}
submittedSubjectIds.add(subjectType.getId());
List<Activity_works_worksType> submittedWorksTypes = CollUtil.defaultIfEmpty(
subjectType.getWorksTypes(), Collections.emptyList());
Set<String> retainedWorksIds = new HashSet<>();
for (Activity_works_worksType worksType : submittedWorksTypes) {
worksType.setSubjectId(subjectType.getId());
if (StrUtil.isBlank(worksType.getId())) {
dao.insert(worksType);
} else {
dao.update(worksType);
}
retainedWorksIds.add(worksType.getId());
}
clearRemovedWorksTypes(subjectType.getId(), retainedWorksIds);
}
clearRemovedSubjectTypes(worksCollection.getId(), submittedSubjectIds);
return null;
}
/**
* 删除页面已移除且未被投稿引用的作品类型;空集合表示删除当前主题下全部原有类型。
*/
private void clearRemovedWorksTypes(String subjectId, Set<String> retainedWorksIds) {
Cnd cnd = Cnd.where(Activity_works_worksType::getSubjectId, "=", subjectId);
if (CollUtil.isNotEmpty(retainedWorksIds)) {
cnd.and(Activity_works_worksType::getId, "not in", retainedWorksIds);
}
dao.clear(Activity_works_worksType.class, cnd);
}
/**
* 删除页面已移除且未被投稿引用的主题类型;空集合表示删除当前活动全部原有主题。
*/
private void clearRemovedSubjectTypes(String activityId, Set<String> retainedSubjectIds) {
Cnd cnd = Cnd.where(Activity_works_subjectType::getActivityId, "=", activityId);
if (CollUtil.isNotEmpty(retainedSubjectIds)) {
cnd.and(Activity_works_subjectType::getId, "not in", retainedSubjectIds);
}
List<String> removedSubjectIds = dao.query(Activity_works_subjectType.class, cnd).stream()
.map(Activity_works_subjectType::getId)
.toList();
if (CollUtil.isEmpty(removedSubjectIds)) {
return;
}
// 先删除未被投稿引用主题下的作品类型,避免产生作品类型孤儿数据。
dao.clear(Activity_works_worksType.class,
Cnd.where(Activity_works_worksType::getSubjectId, "in", removedSubjectIds));
dao.clear(Activity_works_subjectType.class,
Cnd.where(Activity_works_subjectType::getId, "in", removedSubjectIds));
}
@Override @Override
public String validateReferencedTypesBeforeUpdate(Activity_works_collection worksCollection) { public String validateReferencedTypesBeforeUpdate(Activity_works_collection worksCollection) {
if (worksCollection == null || StrUtil.isBlank(worksCollection.getId())) { if (worksCollection == null || StrUtil.isBlank(worksCollection.getId())) {
@@ -218,10 +218,6 @@ public class ClubUserJoinApplyController {
} }
ProcessInstance processInstance = dao.fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", userApply.getId())); ProcessInstance processInstance = dao.fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", userApply.getId()));
// 申请记录存在但没有流程实例时,说明用户仅保存了申请草稿,需提示其先到“我的申请”继续提交。
if (processInstance == null) {
return Result.error(99, "您有该社团的申请记录尚未提交,请到我的申请里查看!");
}
if (!List.of(ProcessInstanceStateEnum.REJECT.getCode(), ProcessInstanceStateEnum.FINISHED.getCode()).contains(processInstance.getState())) { if (!List.of(ProcessInstanceStateEnum.REJECT.getCode(), ProcessInstanceStateEnum.FINISHED.getCode()).contains(processInstance.getState())) {
return Result.error(99, "您有该社团的申请记录尚未完成,请到我的申请里查看!"); return Result.error(99, "您有该社团的申请记录尚未完成,请到我的申请里查看!");
} }
@@ -11,7 +11,6 @@ import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil; import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.constant.FlowConst; import com.budwk.app.flow.constant.FlowConst;
import com.budwk.app.flow.engine.FlowEngine; import com.budwk.app.flow.engine.FlowEngine;
import com.budwk.app.flow.entity.ProcessDefine;
import com.budwk.app.flow.entity.ProcessInstance; import com.budwk.app.flow.entity.ProcessInstance;
import com.budwk.app.flow.entity.ProcessTask; import com.budwk.app.flow.entity.ProcessTask;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum; import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
@@ -96,7 +95,6 @@ public class ClubRefreshReportController {
sys_club_refresh info sys_club_refresh info
LEFT JOIN sys_club club ON club.id = info.clubId LEFT JOIN sys_club club ON club.id = info.clubId
LEFT JOIN `wf_process_instance` ins ON ins.businessNo = info.id LEFT JOIN `wf_process_instance` ins ON ins.businessNo = info.id
AND ins.processDefineId IN (SELECT id FROM wf_process_define WHERE name = 'XHHJBG')
LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10 LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10
LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id
$condition $condition
@@ -180,21 +178,8 @@ public class ClubRefreshReportController {
@SaCheckPermission("club.infoManage.refreshReport") @SaCheckPermission("club.infoManage.refreshReport")
@SLog(tag = "社团管理系统-信息管理", msg = "删除换届报告") @SLog(tag = "社团管理系统-信息管理", msg = "删除换届报告")
public Result delete(@Param("id") String id) { public Result delete(@Param("id") String id) {
// 历史换届记录可能与社团注册共用业务编号,必须按流程定义精确定位换届流程,避免误删注册流程。
List<Long> refreshDefineIds = dao.query(ProcessDefine.class,
Cnd.where(ProcessDefine::getName, "=", "XHHJBG"))
.stream()
.map(ProcessDefine::getId)
.toList();
List<ProcessInstance> refreshInstances = refreshDefineIds.isEmpty()
? List.of()
: dao.query(ProcessInstance.class,
Cnd.where(ProcessInstance::getBusinessNo, "=", id)
.and(ProcessInstance::getProcessDefineId, "in", refreshDefineIds));
clubInfoManageService.dao().delete(SysClubRefresh.class, id); clubInfoManageService.dao().delete(SysClubRefresh.class, id);
for (ProcessInstance refreshInstance : refreshInstances) { flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id);
flowEngine.processInstanceService().deleteProcessInstanceById(refreshInstance.getId());
}
return Result.success(); return Result.success();
} }
} }
@@ -17,8 +17,6 @@ import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.util.cri.SqlExpressionGroup; import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang; import org.nutz.lang.Lang;
@@ -163,26 +161,6 @@ public class SiteCugManageController {
return Result.success(savedInfo == null ? info : savedInfo); return Result.success(savedInfo == null ? info : savedInfo);
} }
/**
* 切换场地开启状态。
*
* @param id 场地ID,必须传入已存在的 site_cug_info 主键
* @param state 目标状态,true 表示开启,false 表示关闭
* @return Result 响应;code 为 0 表示状态更新成功,否则 msg 为参数错误信息
*/
@At
@ApiOperation("切换场地开启状态")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("siteCug.manage")
@SLog(tag = "场馆功能管理-场地管理", msg = "切换场地开启状态")
public Result switchState(String id, Boolean state) {
if (StrUtil.isBlank(id) || state == null) {
return Result.error("场地ID或开启状态不能为空");
}
infoService.updateState(id, state);
return Result.success();
}
@At @At
@ApiOperation("delete") @ApiOperation("delete")
@SaCheckPermission("siteCug.manage") @SaCheckPermission("siteCug.manage")
@@ -4,12 +4,4 @@ import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo; import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo;
public interface SiteCugInfoService extends BaseService<SiteCugInfo> { public interface SiteCugInfoService extends BaseService<SiteCugInfo> {
/**
* 仅更新场地开启状态,不触发场地完整表单校验。
*
* @param id 场地ID
* @param state true 表示开启,false 表示关闭
*/
void updateState(String id, Boolean state);
} }
@@ -4,11 +4,7 @@ import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo; import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo;
import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugInfoService; import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugInfoService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@Slf4j @Slf4j
@@ -18,16 +14,4 @@ public class SiteCugInfoServiceImpl extends BaseServiceImpl<SiteCugInfo> impleme
public SiteCugInfoServiceImpl(Dao dao) { public SiteCugInfoServiceImpl(Dao dao) {
super(dao); super(dao);
} }
/**
* 仅更新指定场地的开启状态,防止开关操作覆盖其他场地配置。
*
* @param id 场地ID
* @param state true 表示开启,false 表示关闭
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateState(String id, Boolean state) {
update(Chain.make("state", state), Cnd.where("id", "=", id));
}
} }
@@ -54,7 +54,7 @@ import java.util.List;
@IocBean @IocBean
@At("/platform/unionReimburse/apply") @At("/platform/unionReimburse/apply")
@Api("工会报销申请") @Api("工会报销申请")
@Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}") @Ok("json:full")
@Slf4j @Slf4j
public class UnionReimburseApplyController { public class UnionReimburseApplyController {
@@ -151,11 +151,14 @@ public class UnionReimburseApplyController {
username as userName, username as userName,
loginname as loginName, loginname as loginName,
sex, sex,
mobile,
technicalTitle, technicalTitle,
IFNULL(unitname, '暂无') as unitName, IFNULL(unitname, '暂无') as unitName,
unitid as unitId, unitid as unitId,
unionid as unionId, unionid as unionId,
unionname as unionName, unionname as unionName,
DATE(birthday) AS birthday,
idCard,
unionCode unionCode
from from
vw_user vw_user
@@ -170,9 +173,6 @@ public class UnionReimburseApplyController {
} }
cnd.and(View_user::getId, "!=", SecurityUtil.getUserId()); cnd.and(View_user::getId, "!=", SecurityUtil.getUserId());
if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
if (StrUtil.isBlank(SecurityUtil.getUnionId())) {
return Result.success(java.util.Collections.emptyList());
}
cnd.and(View_user::getUnionId, "=", SecurityUtil.getUnionId()); cnd.and(View_user::getUnionId, "=", SecurityUtil.getUnionId());
} }
sql.setCondition(cnd); sql.setCondition(cnd);
@@ -34,7 +34,7 @@ import java.util.List;
*/ */
@IocBean @IocBean
@At("/platform/unionReimburse/certifierUserSign") @At("/platform/unionReimburse/certifierUserSign")
@Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}") @Ok("json:full")
@Api("证明人签字") @Api("证明人签字")
@Slf4j @Slf4j
public class UnionReimburseCertifierUserSignController { public class UnionReimburseCertifierUserSignController {
@@ -68,7 +68,7 @@ import java.util.HashMap;
@IocBean @IocBean
@At("/platform/unionReimburse/mine") @At("/platform/unionReimburse/mine")
@Api("工会报销我的") @Api("工会报销我的")
@Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}") @Ok("json:full")
@Slf4j @Slf4j
public class UnionReimburseMineController { public class UnionReimburseMineController {
@@ -29,7 +29,7 @@ import java.util.List;
@IocBean @IocBean
@At("/platform/unionReimburse/review") @At("/platform/unionReimburse/review")
@Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}") @Ok("json:full")
@Api("工会报销审核") @Api("工会报销审核")
public class UnionReimburseReviewController { public class UnionReimburseReviewController {
@@ -38,7 +38,7 @@ import java.util.List;
@IocBean @IocBean
@At("/platform/unionReimburse/statistics") @At("/platform/unionReimburse/statistics")
@Ok("json:{locked:'condolenceIdCard|condolenceBirthday|condolenceMobile',ignoreNull:false}") @Ok("json:full")
@Api("慰问统计") @Api("慰问统计")
@Slf4j @Slf4j
public class UnionReimburseStatisticsController { public class UnionReimburseStatisticsController {
@@ -4,12 +4,10 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.constant.RoleConstant; import com.budwk.app.base.constant.RoleConstant;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_file; import com.budwk.app.sys.models.Sys_file;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.sys.services.SysFileService; import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.zhgh.club.model.ClubUser; import com.budwk.app.zhgh.club.model.ClubUser;
import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub; import com.budwk.app.zhgh.dayofficework.outlay.outlayManage.club.model.OutlayManageClub;
@@ -127,37 +125,6 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
if (oldRecord == null) { if (oldRecord == null) {
return Result.error("未找到对应的报销记录"); return Result.error("未找到对应的报销记录");
} }
if (!isAdmin() && !Objects.equals(oldRecord.getUserId(), SecurityUtil.getUserId())) {
return Result.error("无权修改此报销申请");
}
}
// 人员身份和敏感资料由服务端补齐,避免前端删减字段导致丢失或被篡改。
if (oldRecord == null) {
View_user handler = dao().fetch(View_user.class, Cnd.where("id", "=", SecurityUtil.getUserId()));
if (handler == null) {
return Result.error("经办人信息不存在");
}
unionReimburse.setUserId(handler.getId());
unionReimburse.setUserName(handler.getUsername());
unionReimburse.setLoginName(handler.getLoginname());
unionReimburse.setUnitId(handler.getUnitId());
unionReimburse.setUnitName(handler.getUnitName());
unionReimburse.setUnionId(handler.getUnionId());
unionReimburse.setUnionName(handler.getUnionName());
} else {
// 修改历史单据时保留原经办组织,避免人员调动改变经费归属。
unionReimburse.setUserId(oldRecord.getUserId());
unionReimburse.setUserName(oldRecord.getUserName());
unionReimburse.setLoginName(oldRecord.getLoginName());
unionReimburse.setUnitId(oldRecord.getUnitId());
unionReimburse.setUnitName(oldRecord.getUnitName());
unionReimburse.setUnionId(oldRecord.getUnionId());
unionReimburse.setUnionName(oldRecord.getUnionName());
}
Result recipientResult = fillCondolencePersonalData(unionReimburse, oldRecord);
if (recipientResult != null) {
return recipientResult;
} }
normalizeInvoiceDetails(unionReimburse); normalizeInvoiceDetails(unionReimburse);
@@ -212,104 +179,14 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
if (unionReimburse == null) { if (unionReimburse == null) {
return null; return null;
} }
if (!canReadApply(unionReimburse)) {
throw new BaseException("无权查看此报销申请");
}
List<UnionReimburseInvoiceDetail> invoiceDetails = this.dao().query( List<UnionReimburseInvoiceDetail> invoiceDetails = this.dao().query(
UnionReimburseInvoiceDetail.class, UnionReimburseInvoiceDetail.class,
Cnd.where("reimburseId", "=", id).asc("createdAt") Cnd.where("reimburseId", "=", id).asc("createdAt")
); );
unionReimburse.setInvoiceDetails(invoiceDetails == null ? new ArrayList<>() : invoiceDetails); unionReimburse.setInvoiceDetails(invoiceDetails == null ? new ArrayList<>() : invoiceDetails);
// 申请页和详情页共用此接口,原始身份资料仅保留在服务端业务记录中。
unionReimburse.setCondolenceIdCard(null);
unionReimburse.setCondolenceBirthday(null);
unionReimburse.setCondolenceMobile(null);
return unionReimburse; return unionReimburse;
} }
/** 保留历史单据的身份资料快照,新选人员须符合人员查询的数据范围。 */
private Result fillCondolencePersonalData(UnionReimburse form, UnionReimburse oldRecord) {
form.setCondolenceIdCard(null);
form.setCondolenceBirthday(null);
form.setCondolenceMobile(null);
if (!"UNION_REIMBURSE_PROJECT_1".equals(form.getReimburseProject())) {
return null;
}
if (StrUtil.isBlank(form.getCondolenceUserId())) {
return null;
}
if (oldRecord != null && Objects.equals(oldRecord.getCondolenceUserId(), form.getCondolenceUserId())) {
form.setCondolenceIdCard(oldRecord.getCondolenceIdCard());
form.setCondolenceBirthday(oldRecord.getCondolenceBirthday());
form.setCondolenceMobile(oldRecord.getCondolenceMobile());
form.setCondolenceUnionId(oldRecord.getCondolenceUnionId());
form.setCondolenceUnitId(oldRecord.getCondolenceUnitId());
form.setCondolenceUserName(oldRecord.getCondolenceUserName());
form.setCondolenceLoginName(oldRecord.getCondolenceLoginName());
form.setCondolenceUnitName(oldRecord.getCondolenceUnitName());
form.setCondolenceUnionName(oldRecord.getCondolenceUnionName());
form.setCondolenceUnionCode(oldRecord.getCondolenceUnionCode());
form.setCondolenceSex(oldRecord.getCondolenceSex());
return null;
}
View_user user = dao().fetch(View_user.class, Cnd.where("id", "=", form.getCondolenceUserId()));
if (user == null || Objects.equals(user.getId(), SecurityUtil.getUserId())
|| (!isAdmin() && (StrUtil.isBlank(SecurityUtil.getUnionId())
|| !Objects.equals(SecurityUtil.getUnionId(), user.getUnionId())))) {
return Result.error("无权选择此慰问对象");
}
form.setCondolenceUserName(user.getUsername());
form.setCondolenceLoginName(user.getLoginname());
form.setCondolenceUnitId(user.getUnitId());
form.setCondolenceUnitName(user.getUnitName());
form.setCondolenceUnionId(user.getUnionId());
form.setCondolenceUnionName(user.getUnionName());
form.setCondolenceUnionCode(user.getUnionCode());
form.setCondolenceSex(user.getSex());
form.setCondolenceIdCard(user.getIdCard());
form.setCondolenceBirthday(user.getBirthday() == null ? null : DateUtil.formatDate(user.getBirthday()));
form.setCondolenceMobile(user.getMobile());
return null;
}
/** 详情访问沿用经办、审核、统计和流程参与范围,不能仅凭单据 ID 查询。 */
private boolean canReadApply(UnionReimburse form) {
String userId = SecurityUtil.getUserId();
if (StrUtil.isBlank(userId)) {
return false;
}
if (isAdmin() || userId.equals(form.getUserId())) {
return true;
}
if (form.getStateId() != 1 && (AuthUtil.hasPermission("unionReimburse.review")
|| AuthUtil.hasPermission("h5.unionReimburse.review"))) {
return true;
}
if ((form.getStateId() == 2 || form.getStateId() == 3)
&& StrUtil.isNotBlank(SecurityUtil.getUnionId())
&& Objects.equals(SecurityUtil.getUnionId(), form.getCondolenceUnionId())
&& (AuthUtil.hasPermission("unionReimburse.statistics")
|| AuthUtil.hasPermission("h5.unionReimburse.statistics"))) {
return true;
}
// 旧流程单据允许任务参与人和抄送人回看,关系必须由数据库确认。
Sql sql = Sqls.create("""
SELECT COUNT(*) FROM wf_process_instance i
WHERE i.businessNo = @id AND (
EXISTS (SELECT 1 FROM wf_process_task t
INNER JOIN wf_process_task_actor a ON a.processTaskId = t.id
WHERE t.processInstanceId = i.id AND a.actorId = @userId)
OR EXISTS (SELECT 1 FROM wf_process_cc_instance c
WHERE c.processInstanceId = i.id AND c.actorId = @userId)
)
""");
sql.setParam("id", form.getId());
sql.setParam("userId", userId);
sql.setCallback(Sqls.callback.integer());
dao().execute(sql);
return sql.getInt() > 0;
}
@Override @Override
public void deleteApply(String id) { public void deleteApply(String id) {
if (StrUtil.isBlank(id)) { if (StrUtil.isBlank(id)) {
@@ -365,11 +242,14 @@ public class UnionReimburseServiceImpl extends BaseServiceImpl<UnionReimburse> i
u.username AS userName, u.username AS userName,
u.loginname AS loginName, u.loginname AS loginName,
u.sex, u.sex,
u.mobile,
u.technicalTitle, u.technicalTitle,
IFNULL(u.unitname, '暂无') AS unitName, IFNULL(u.unitname, '暂无') AS unitName,
u.unitid AS unitId, u.unitid AS unitId,
u.unionid AS unionId, u.unionid AS unionId,
u.unionname AS unionName, u.unionname AS unionName,
DATE(u.birthday) AS birthday,
u.idCard,
u.unionCode u.unionCode
FROM FROM
vw_user u vw_user u
@@ -91,7 +91,6 @@ public class CongressVotingStatisticalController {
@At("/records") @At("/records")
@SaCheckPermission("congress.vote.statistical") @SaCheckPermission("congress.vote.statistical")
public Result voteRecords(@Param("issueId") String issueId) { public Result voteRecords(@Param("issueId") String issueId) {
// H5 投票使用现有职代会代表表写入 rep_id,旧表关联仅作为历史数据兼容回退。
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
r.id, r.id,
@@ -99,17 +98,10 @@ public class CongressVotingStatisticalController {
r.name, r.name,
r.vote_result AS voteResult, r.vote_result AS voteResult,
r.create_time AS createTime, r.create_time AS createTime,
COALESCE(tc_delegate.unionId, rep.union_id, du.union_id) AS unionId, COALESCE(rep.union_id, du.union_id) AS unionId,
COALESCE(tc_delegate.unionName, du.union_name) AS unionName, du.union_name AS unionName,
COALESCE(delegate_delegation.name, rep.delegation_name, d.name) AS delegationName COALESCE(rep.delegation_name, d.name) AS delegationName
FROM congress_voting_record r FROM congress_voting_record r
LEFT JOIN teacher_congress_delegate tc_delegate ON tc_delegate.id = r.rep_id
AND tc_delegate.sessionId = r.session_id
AND COALESCE(tc_delegate.delFlag, 0) = 0
LEFT JOIN teacher_congress_delegation delegate_delegation
ON delegate_delegation.id = COALESCE(r.delegation_id, tc_delegate.delegationId)
AND delegate_delegation.sessionId = r.session_id
AND COALESCE(delegate_delegation.delFlag, 0) = 0
LEFT JOIN congress_rep rep ON rep.id = r.rep_id LEFT JOIN congress_rep rep ON rep.id = r.rep_id
LEFT JOIN congress_delegation d ON d.id = r.delegation_id LEFT JOIN congress_delegation d ON d.id = r.delegation_id
LEFT JOIN ( LEFT JOIN (
@@ -173,7 +173,10 @@ public class ProposalSecondedController {
} }
ProposalSearchParam.buildSearch(cnd, pageForm); ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id"); cnd.groupBy("t.id");
// 用户未指定有效排序字段时,继续沿用原有任务创建时间倒序。
if (!proposalCommonService.applySafePageOrder(cnd, pageForm, ORDER_COLUMNS)) {
cnd.desc("t.createdAt"); cnd.desc("t.createdAt");
}
sql.setCondition(cnd); sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination); return Result.success(pagination);
@@ -244,7 +244,7 @@ public class ProposalUnderTakeReplyController {
@SaCheckPermission("proposal.unitReply") @SaCheckPermission("proposal.unitReply")
@ApiOperation("查询本单位可转办用户") @ApiOperation("查询本单位可转办用户")
public Result selectViceUser(@Param("keyword") @Valid String keyword) { public Result selectViceUser(@Param("keyword") @Valid String keyword) {
Sql sql = Sqls.create("select id,loginname,username,unitName from vw_user $condition"); Sql sql = Sqls.create("select id,loginname,username from vw_user $condition");
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.andEX("unitId", "=", SecurityUtil.getUnitId()); cnd.andEX("unitId", "=", SecurityUtil.getUnitId());
cnd.andEX("id", "!=", SecurityUtil.getUserId()); cnd.andEX("id", "!=", SecurityUtil.getUserId());
@@ -79,7 +79,7 @@ public class ProposalSearchParam extends PageForm {
if (StrUtil.isAllNotBlank(searchParam.getPageOrderName(), searchParam.getPageOrderBy())) { if (StrUtil.isAllNotBlank(searchParam.getPageOrderName(), searchParam.getPageOrderBy())) {
cnd.orderBy(searchParam.getPageOrderName(), PageUtil.getOrder(searchParam.getPageOrderBy())); cnd.orderBy(searchParam.getPageOrderName(), PageUtil.getOrder(searchParam.getPageOrderBy()));
} else { } else {
cnd.desc("info.code"); cnd.asc("info.code");
} }
} }
} }
@@ -11,6 +11,7 @@ import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.eclipse.jetty.util.StringUtil;
import org.nutz.dao.entity.annotation.*; import org.nutz.dao.entity.annotation.*;
import org.nutz.dao.interceptor.annotation.PrevInsert; import org.nutz.dao.interceptor.annotation.PrevInsert;
@@ -160,13 +161,13 @@ public class MallGoods extends BaseModel {
//循环sku,判定sku是否有效 //循环sku,判定sku是否有效
for (Map<String, Object> sku : goodsOperationDTO.getSkuList()) { for (Map<String, Object> sku : goodsOperationDTO.getSkuList()) {
//判定参数不能为空 //判定参数不能为空
if ((!sku.containsKey("price") || CharSequenceUtil.isEmpty(sku.get("price").toString()) || Convert.toDouble(sku.get("price")) <= 0)) { if ((!sku.containsKey("price") || StringUtil.isEmpty(sku.get("price").toString()) || Convert.toDouble(sku.get("price")) <= 0)) {
throw new BaseException("商品SKU价格不能小于等于0"); throw new BaseException("商品SKU价格不能小于等于0");
} }
// if ((!sku.containsKey("cost") || StringUtil.isEmpty(sku.get("cost").toString()) || Convert.toDouble(sku.get("cost")) <= 0)) { // if ((!sku.containsKey("cost") || StringUtil.isEmpty(sku.get("cost").toString()) || Convert.toDouble(sku.get("cost")) <= 0)) {
// throw new BaseException("商品SKU成本价不能小于等于0"); // throw new BaseException("商品SKU成本价不能小于等于0");
// } // }
if (!sku.containsKey("quantity") || CharSequenceUtil.isEmpty(sku.get("quantity").toString()) || Convert.toInt(sku.get("quantity").toString()) < 0) { if (!sku.containsKey("quantity") || StringUtil.isEmpty(sku.get("quantity").toString()) || Convert.toInt(sku.get("quantity").toString()) < 0) {
throw new BaseException("商品库存数量不能为负数"); throw new BaseException("商品库存数量不能为负数");
} }
sku.values().forEach(i -> { sku.values().forEach(i -> {
@@ -37,13 +37,34 @@ public class PointsMallInvoiceController {
@At @At
@SaCheckPermission("points.mall.invoice") @SaCheckPermission("points.mall.invoice")
public Result infoPage(PointsMallInvoiceInfoPageParam param) { public Result infoPage(PointsMallInvoiceInfoPageParam param) {
try {
return Result.success(pointsMallInvoiceService.infoPage(param)); return Result.success(pointsMallInvoiceService.infoPage(param));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
} }
@At @At
@SaCheckPermission("points.mall.invoice") @SaCheckPermission("points.mall.invoice")
public Result infoDetail(@Param("id") String id) { public Result infoDetail(@Param("id") String id) {
try {
return Result.success(pointsMallInvoiceService.infoDetail(id)); return Result.success(pointsMallInvoiceService.infoDetail(id));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
@At
@org.nutz.mvc.annotation.POST
@SLog(tag = "积分商城发票管理", msg = "重新获取发票")
@SaCheckPermission("points.mall.invoice")
public Result retryInvoice(@Param("mainId") String mainId) {
try {
pointsMallInvoiceService.retryInvoice(mainId);
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
} }
@At @At
@@ -16,6 +16,9 @@ import java.util.Date;
@Comment("积分商城发票信息") @Comment("积分商城发票信息")
public class PointsMallInvoiceInfo extends BaseModel implements Serializable { public class PointsMallInvoiceInfo extends BaseModel implements Serializable {
// 详情实时读取供应商 PDF 生成结果:1 成功,2 失败,不以本地文件地址推断状态。
private Integer status;
@Name @Name
@Column @Column
@Comment("ID") @Comment("ID")
@@ -7,11 +7,13 @@ public class PointsMallInvoiceApplyParam {
private String supplierId; private String supplierId;
private String orderIds; private String orderIds;
private String queryOrderId;
private String userId;
private String startDate; private String startDate;
private String endDate; private String endDate;
private String invoiceDate; private String invoiceDate;
private String invoiceTitle; private String invoiceTitle;
private String invoiceCode; private String taxIdNumber;
private String invoiceAddress; private String invoiceAddress;
private String invoiceContact; private String invoiceContact;
private Integer invoiceType; private Integer invoiceType;
@@ -3,6 +3,9 @@ package com.budwk.app.zhgh.pointsmall.invoice.service;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.BaseService; import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceMain; import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceMain;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceInfo;
import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceApplyParam; import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceApplyParam;
import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceInfoPageParam; import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceInfoPageParam;
import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceMainPageParam; import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceMainPageParam;
@@ -15,11 +18,19 @@ import java.util.List;
public interface PointsMallInvoiceService extends BaseService<PointsMallInvoiceMain> { public interface PointsMallInvoiceService extends BaseService<PointsMallInvoiceMain> {
/** 同步供应商发票信息及本地文件。 */
MallInboundResult invoiceInfo(MallOrderEventDTO dto);
/** 同步发票,可指定是否重新生成失败发票。 */
MallInboundResult invoiceInfo(MallOrderEventDTO dto, boolean retry);
Pagination<PointsMallInvoiceMain> mainPage(PointsMallInvoiceMainPageParam param); Pagination<PointsMallInvoiceMain> mainPage(PointsMallInvoiceMainPageParam param);
Pagination<Record> infoPage(PointsMallInvoiceInfoPageParam param); Pagination<Record> infoPage(PointsMallInvoiceInfoPageParam param);
Record infoDetail(String id); PointsMallInvoiceInfo infoDetail(String id);
void retryInvoice(String mainId);
Pagination<Record> orderPage(PointsMallInvoiceOrderPageParam param); Pagination<Record> orderPage(PointsMallInvoiceOrderPageParam param);
@@ -6,39 +6,207 @@ import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceInfo;
import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceMain; import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceMain;
import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceApplyParam; import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceApplyParam;
import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceInfoPageParam; import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceInfoPageParam;
import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceMainPageParam; import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceMainPageParam;
import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceOrderPageParam; import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceOrderPageParam;
import com.budwk.app.zhgh.pointsmall.invoice.service.PointsMallInvoiceService; import com.budwk.app.zhgh.pointsmall.invoice.service.PointsMallInvoiceService;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallInvoiceApplyDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.InvoiceInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.QueryInvoiceInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.GetInvoiceInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallOutboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallInvoiceApplyOutboundService;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallInvoiceInfoQueryService;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub; import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier; import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import org.nutz.dao.Chain; import org.nutz.dao.Chain;
import org.nutz.trans.Atom;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.entity.Record; import org.nutz.dao.entity.Record;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.trans.Trans;
import java.math.BigDecimal; import java.math.BigDecimal;
import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceInfo;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.sys.enums.SysFileEngineTypeEnum;
import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallPictureOutboundResult;
import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvoiceMain> implements PointsMallInvoiceService { public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvoiceMain> implements PointsMallInvoiceService {
@Inject
private MallInvoiceApplyOutboundService mallInvoiceApplyOutboundService;
@Inject
private SysFileService sysFileService;
@Inject
private MallInvoiceInfoQueryService mallInvoiceInfoQueryService;
public PointsMallInvoiceServiceImpl(Dao dao) { public PointsMallInvoiceServiceImpl(Dao dao) {
super(dao); super(dao);
} }
/** 处理供应商发票信息通知,查询发票并同步文件和开票结果。 */
@Override
public MallInboundResult invoiceInfo(MallOrderEventDTO dto) {
return invoiceInfo(dto, false);
}
/** retry=true 时允许供应商重新生成失败发票,后续复用相同的同步流程。 */
@Override
public MallInboundResult invoiceInfo(MallOrderEventDTO dto, boolean retry) {
String settlementId = dto.getOrderId();
if (StrUtil.hasBlank(dto.getSupplierId(), settlementId)) {
return MallInboundResult.fail(400, "供应商和结算单号不能为空");
}
QueryInvoiceInfoDTO query = new QueryInvoiceInfoDTO();
query.setSupplierId(dto.getSupplierId());
query.setSettlementId(settlementId);
query.setRetry(retry);
GetInvoiceInfoDTO result;
try {
result = mallInvoiceInfoQueryService.queryInvoicesInfo(query);
} catch (Exception e) {
log.error("调用供应商获取发票信息失败,supplierId={}settlementId={}", dto.getSupplierId(), settlementId, e);
return MallInboundResult.fail(-1, "获取供应商发票信息失败");
}
PointsMallInvoiceMain main = dao().fetch(PointsMallInvoiceMain.class,
Cnd.where("settlementId", "=", settlementId).and("delFlag", "=", false));
if (main == null) {
return MallInboundResult.fail(404, "开票申请记录不存在");
}
if (result == null || !settlementId.equals(result.getSettlementId())) {
return MallInboundResult.fail(502, "供应商返回的发票结算单不匹配");
}
Integer success = result.getBSuccess() == null ? 0 : result.getBSuccess();
if (success == 0) {
if (result.getInvoiceInfos() == null || result.getInvoiceInfos().isEmpty() || StrUtil.isBlank(result.getSucOrderIds())) {
return MallInboundResult.fail(502, "供应商开票结果缺少发票明细或成功订单");
}
try {
List<String> fileIds = result.getInvoiceInfos().stream().map(InvoiceInfoDTO::getInvoiceId).collect(Collectors.toList());
if (fileIds.stream().anyMatch(StrUtil::isBlank)) {
throw new IllegalArgumentException("供应商发票文件标识为空");
}
MallPictureOutboundResult files = mallInvoiceInfoQueryService.queryInvoiceFiles(dto.getSupplierId(), settlementId, fileIds);
for (InvoiceInfoDTO item : result.getInvoiceInfos()) {
if (item.getStatus() != null && item.getStatus() != 1) {
throw new IllegalArgumentException("供应商发票尚未生成成功");
}
String base64 = files.getImageEncodeMap() == null ? null : files.getImageEncodeMap().get(item.getInvoiceId());
if (StrUtil.isBlank(base64)) {
throw new IllegalArgumentException("未获取到发票文件Base64" + item.getInvoiceId());
}
if (base64.startsWith("data:")) {
base64 = base64.substring(base64.indexOf(',') + 1);
}
byte[] bytes = Base64.getDecoder().decode(base64.replaceAll("\\s", ""));
if (bytes.length == 0) {
throw new IllegalArgumentException("发票文件内容为空");
}
String fileType = StrUtil.blankToDefault(files.getFileType(), StrUtil.blankToDefault(item.getFileType(), "PDF"));
if (!fileType.matches("(?i)pdf|jpg|jpeg|png")) {
throw new IllegalArgumentException("不支持的发票文件类型:" + fileType);
}
String localUrl = sysFileService.uploadReturnUrl(SysFileEngineTypeEnum.MINIO.getValue(),
"invoice-" + java.util.UUID.randomUUID() + "." + fileType.toLowerCase(java.util.Locale.ROOT), bytes);
item.setUrl(localUrl);
item.setImageEncode(localUrl);
item.setFileType(fileType);
}
} catch (Exception e) {
log.error("发票文件同步失败,settlementId={}", settlementId, e);
return MallInboundResult.fail(502, "发票文件同步失败:" + e.getMessage());
}
}
Trans.exec((Atom) () -> {
main.setBSuccess(success);
main.setSucOrderIds(result.getSucOrderIds());
main.setFailOrderIds(result.getFailOrderIds());
main.setFailMsg(result.getFailMsg());
if (result.getInvoiceInfos() != null) {
for (InvoiceInfoDTO item : result.getInvoiceInfos()) {
PointsMallInvoiceInfo info = dao().fetch(PointsMallInvoiceInfo.class,
Cnd.where("mainId", "=", main.getId()).and("invoiceId", "=", item.getInvoiceId()).and("delFlag", "=", false));
if (info == null) {
info = new PointsMallInvoiceInfo();
info.setMainId(main.getId());
info.setInvoiceId(item.getInvoiceId());
}
info.setInvoiceCode(item.getInvoiceCode());
info.setInvoiceDate(item.getInvoiceDate() == null ? null : DateUtil.parse(item.getInvoiceDate().toString()));
info.setInvoiceNakeAmount(item.getInvoiceNakeAmount());
info.setInvoiceTaxRate(item.getInvoiceTaxRate());
info.setInvoiceTaxAmount(item.getInvoiceTaxAmount());
info.setInvoiceAmount(item.getInvoiceAmount());
info.setInvoiceType(item.getInvoiceType());
info.setUrl(item.getUrl());
info.setImageEncode(item.getImageEncode());
info.setFileType(item.getFileType());
info.setCheckCode(item.getCheckCode());
info.setInvoiceAddress(item.getInvoiceAddress());
info.setRemark(item.getRemark());
if (success != 0) {
info.setUrl("");
info.setImageEncode("");
}
if (StrUtil.isBlank(info.getId())) {
dao().insert(info);
} else {
dao().updateIgnoreNull(info);
}
}
}
List<String> successOrderIds = splitInvoiceOrderIds(result.getSucOrderIds());
List<String> failOrderIds = splitInvoiceOrderIds(result.getFailOrderIds());
if (!successOrderIds.isEmpty()) {
dao().update(PointsMallOrderSub.class, Chain.make("invoiceStatus", 1), Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "in", successOrderIds).and("delFlag", "=", false));
}
if (!failOrderIds.isEmpty()) {
dao().update(PointsMallOrderSub.class, Chain.make("invoiceStatus", 0), Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "in", failOrderIds).and("delFlag", "=", false));
}
dao().updateIgnoreNull(main);
});
log.info("供应商发票信息更新完成,supplierId={}settlementId={}", dto.getSupplierId(), settlementId);
return MallInboundResult.success();
}
/** 拆分开票结果中的订单号,去除空项和重复项,保留原有顺序。 */
private List<String> splitInvoiceOrderIds(String value) {
if (StrUtil.isBlank(value)) {
return Collections.emptyList();
}
return Arrays.stream(value.split(","))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
}
@Override @Override
public Pagination<PointsMallInvoiceMain> mainPage(PointsMallInvoiceMainPageParam param) { public Pagination<PointsMallInvoiceMain> mainPage(PointsMallInvoiceMainPageParam param) {
Cnd cnd = Cnd.where("delFlag", "=", false); Cnd cnd = Cnd.where("delFlag", "=", false);
if (StrUtil.isNotBlank(param.getSettlementId())) { if (StrUtil.isNotBlank(param.getSettlementId())) {
cnd.and("settlementId", "like", "%" + param.getSettlementId() + "%"); cnd.and("settlementId", "=", param.getSettlementId());
} }
if (param.getBSuccess() != null) { if (param.getBSuccess() != null) {
cnd.and("bSuccess", "=", param.getBSuccess()); cnd.and("bSuccess", "=", param.getBSuccess());
@@ -58,15 +226,32 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
@Override @Override
public Pagination<Record> infoPage(PointsMallInvoiceInfoPageParam param) { public Pagination<Record> infoPage(PointsMallInvoiceInfoPageParam param) {
if (StrUtil.isNotBlank(param.getMainId()) && dao().count(PointsMallInvoiceInfo.class,
Cnd.where("mainId", "=", param.getMainId()).and("delFlag", "=", false)) == 0) {
PointsMallInvoiceMain main = fetch(param.getMainId());
PointsMallOrderSub order = main == null ? null : dao().fetch(PointsMallOrderSub.class,
Cnd.where("invoiceCode", "=", main.getSettlementId()).and("delFlag", "=", false));
if (order != null) {
MallOrderEventDTO event = new MallOrderEventDTO();
event.setSupplierId(order.getSupplierId());
event.setOrderId(main.getSettlementId());
MallInboundResult result = invoiceInfo(event);
if (result.getResultCode() != 0) {
dao().update(PointsMallInvoiceMain.class, Chain.make("bSuccess", 1).add("failMsg", result.getMessage()),
Cnd.where("id", "=", main.getId()));
throw new IllegalArgumentException(result.getMessage());
}
}
}
Cnd cnd = Cnd.where("delFlag", "=", false); Cnd cnd = Cnd.where("delFlag", "=", false);
if (StrUtil.isNotBlank(param.getMainId())) { if (StrUtil.isNotBlank(param.getMainId())) {
cnd.and("mainId", "=", param.getMainId()); cnd.and("mainId", "=", param.getMainId());
} }
if (StrUtil.isNotBlank(param.getInvoiceId())) { if (StrUtil.isNotBlank(param.getInvoiceId())) {
cnd.and("invoiceId", "like", "%" + param.getInvoiceId() + "%"); cnd.and("invoiceId", "=", param.getInvoiceId());
} }
if (StrUtil.isNotBlank(param.getInvoiceCode())) { if (StrUtil.isNotBlank(param.getInvoiceCode())) {
cnd.and("invoiceCode", "like", "%" + param.getInvoiceCode() + "%"); cnd.and("invoiceCode", "=", param.getInvoiceCode());
} }
if (StrUtil.isNotBlank(param.getInvoiceDate())) { if (StrUtil.isNotBlank(param.getInvoiceDate())) {
cnd.and("invoiceDate", "=", DateUtil.parse(param.getInvoiceDate())); cnd.and("invoiceDate", "=", DateUtil.parse(param.getInvoiceDate()));
@@ -81,13 +266,61 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
} }
@Override @Override
public Record infoDetail(String id) { public PointsMallInvoiceInfo infoDetail(String id) {
if (StrUtil.isBlank(id)) { if (StrUtil.isBlank(id)) {
return null; return null;
} }
Sql sql = Sqls.create("select * from points_mall_invoice_info where id=@id and delFlag=0"); PointsMallInvoiceInfo info = dao().fetch(PointsMallInvoiceInfo.class, Cnd.where("id", "=", id).and("delFlag", "=", false));
sql.params().set("id", id); if (info == null) {
return (Record) dao().execute(sql.setCallback(Sqls.callback.record())).getResult(); return null;
}
MallOrderEventDTO event = invoiceEvent(info.getMainId());
QueryInvoiceInfoDTO query = new QueryInvoiceInfoDTO();
query.setSupplierId(event.getSupplierId());
query.setSettlementId(event.getOrderId());
GetInvoiceInfoDTO result = mallInvoiceInfoQueryService.queryInvoicesInfo(query);
if (result == null || !event.getOrderId().equals(result.getSettlementId()) || result.getInvoiceInfos() == null) {
throw new IllegalArgumentException("供应商未返回对应结算单的发票详情");
}
InvoiceInfoDTO remote = result.getInvoiceInfos().stream()
.filter(item -> info.getInvoiceId().equals(item.getInvoiceId())).findFirst()
.orElseThrow(() -> new IllegalArgumentException("供应商未返回对应发票详情"));
info.setStatus(remote.getStatus());
info.setRemark(remote.getRemark());
if (!Integer.valueOf(1).equals(remote.getStatus())) {
info.setUrl("");
info.setImageEncode("");
}
return info;
}
private MallOrderEventDTO invoiceEvent(String mainId) {
PointsMallInvoiceMain main = StrUtil.isBlank(mainId) ? null : dao().fetch(PointsMallInvoiceMain.class,
Cnd.where("id", "=", mainId).and("delFlag", "=", false));
if (main == null) {
throw new IllegalArgumentException("开票申请记录不存在");
}
PointsMallOrderSub order = dao().fetch(PointsMallOrderSub.class,
Cnd.where("invoiceCode", "=", main.getSettlementId()).and("delFlag", "=", false));
if (order == null) {
throw new IllegalArgumentException("未找到结算单对应的供应商订单");
}
MallOrderEventDTO event = new MallOrderEventDTO();
event.setSupplierId(order.getSupplierId());
event.setOrderId(main.getSettlementId());
return event;
}
@Override
public void retryInvoice(String mainId) {
MallInboundResult result = invoiceInfo(invoiceEvent(mainId), true);
if (result.getResultCode() != 0) {
throw new IllegalArgumentException(result.getMessage());
}
PointsMallInvoiceMain main = fetch(mainId);
if (!Integer.valueOf(0).equals(main.getBSuccess())) {
throw new IllegalArgumentException(StrUtil.blankToDefault(main.getFailMsg(), "供应商发票生成失败"));
}
} }
@Override @Override
@@ -117,7 +350,19 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
query.setEndDate(param.getEndDate()); query.setEndDate(param.getEndDate());
query.setInvoiceStatus(0); query.setInvoiceStatus(0);
List<PointsMallOrderSub> orders = dao().query(PointsMallOrderSub.class, invoiceOrderCnd(query, "")); Cnd cnd = invoiceOrderCnd(query, "");
if (StrUtil.isNotBlank(param.getQueryOrderId())) {
List<String> queryOrderIds = StrUtil.splitTrim(param.getQueryOrderId(), ",");
if (queryOrderIds.size() > 1) {
cnd.and("orderId", "in", queryOrderIds);
} else {
cnd.and("orderId", "like", "%" + param.getQueryOrderId() + "%");
}
}
if (StrUtil.isNotBlank(param.getUserId())) {
cnd.and("userId", "like", "%" + param.getUserId() + "%");
}
List<PointsMallOrderSub> orders = dao().query(PointsMallOrderSub.class, cnd);
if (CollUtil.isEmpty(orders)) { if (CollUtil.isEmpty(orders)) {
throw new IllegalArgumentException("无符合条件的订单数据"); throw new IllegalArgumentException("无符合条件的订单数据");
} }
@@ -126,30 +371,69 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
String settlementId = System.currentTimeMillis() + RandomUtil.randomNumbers(6); String settlementId = System.currentTimeMillis() + RandomUtil.randomNumbers(6);
PointsMallInvoiceMain invoice = new PointsMallInvoiceMain(); PointsMallInvoiceMain invoice = new PointsMallInvoiceMain();
invoice.setSettlementId(settlementId); invoice.setSettlementId(settlementId);
invoice.setBSuccess(0); invoice.setBSuccess(null);
invoice.setSucOrderIds(String.join(",", orderIds)); invoice.setSucOrderIds("");
invoice.setFailOrderIds(""); invoice.setFailOrderIds("");
invoice.setFailMsg(""); invoice.setFailMsg("");
insert(invoice); insert(invoice);
PointsMallInvoiceInfo info = new PointsMallInvoiceInfo(); MallInvoiceApplyDTO dto = new MallInvoiceApplyDTO();
info.setMainId(invoice.getId()); dto.setSupplierId(param.getSupplierId());
info.setInvoiceId(settlementId); dto.setSettlementId(settlementId);
info.setInvoiceCode(param.getInvoiceCode()); dto.setOrderIds(String.join(",", orderIds));
info.setInvoiceDate(DateUtil.parse(param.getInvoiceDate())); dto.setOrderNum(orders.size());
info.setInvoiceType(param.getInvoiceType()); dto.setOrderTotalPrice(orders.stream().map(PointsMallOrderSub::getTotalPrice)
info.setInvoiceAmount(orderTotalPoints(query)); .filter(java.util.Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
info.setInvoiceNakeAmount(info.getInvoiceAmount()); dto.setInvoiceDate(param.getInvoiceDate());
info.setInvoiceAddress(param.getInvoiceAddress()); dto.setInvoiceTitle(param.getInvoiceTitle());
info.setInvoiceContact(param.getInvoiceContact()); dto.setTaxIdNumber(param.getTaxIdNumber());
info.setInvoiceContent(param.getInvoiceContent()); dto.setInvoiceAddress(param.getInvoiceAddress());
info.setTaxIdNumber(param.getInvoiceCode()); dto.setInvoiceContact(param.getInvoiceContact());
info.setRemark(param.getRemark()); dto.setInvoiceType(param.getInvoiceType() == null ? null : String.valueOf(param.getInvoiceType()));
info.setInvoiceBank(param.getBankName()); dto.setInvoiceContent(param.getInvoiceContent());
info.setInvoiceAccount(param.getBankAccount()); dto.setRemark(param.getRemark());
dao().insert(info); dto.setBankAccount(param.getBankAccount());
dto.setRegisteredAddress(param.getRegisteredAddress());
dto.setBankName(param.getBankName());
dao().update(PointsMallOrderSub.class, Chain.make("invoiceStatus", 2).add("invoiceCode", settlementId),
Cnd.where("supplierId", "=", param.getSupplierId()).and("orderId", "in", orderIds));
MallOutboundResult result;
boolean requestFailed = false;
try {
result = mallInvoiceApplyOutboundService.applyInvoice(dto);
} catch (Exception e) {
requestFailed = true;
result = MallOutboundResult.fail(502, e.getMessage());
}
if (result == null || result.getResultCode() != 0) {
PointsMallInvoiceMain current = fetch(invoice.getId());
if (current != null && Integer.valueOf(0).equals(current.getBSuccess())) {
return "开票已完成,结算单号:" + settlementId;
}
if (!requestFailed && result != null && result.getResultCode() != 502) {
Trans.exec((Atom) () -> {
dao().update(PointsMallInvoiceInfo.class, Chain.make("delFlag", true),
Cnd.where("mainId", "=", invoice.getId()));
dao().update(PointsMallInvoiceMain.class, Chain.make("delFlag", true),
Cnd.where("id", "=", invoice.getId()));
dao().update(PointsMallOrderSub.class, Chain.make("invoiceStatus", 0).add("invoiceCode", ""),
Cnd.where("supplierId", "=", param.getSupplierId()).and("orderId", "in", orderIds)
.and("invoiceCode", "=", settlementId).and("invoiceStatus", "in", new Integer[]{0, 2}));
});
throw new IllegalArgumentException(result.getMessage());
}
invoice.setBSuccess(1);
invoice.setSucOrderIds("");
invoice.setFailOrderIds(String.join(",", orderIds));
invoice.setFailMsg(result == null ? "供应商开票申请无响应" : result.getMessage());
updateIgnoreNull(invoice);
dao().update(PointsMallOrderSub.class, Chain.make("invoiceStatus", 0),
Cnd.where("supplierId", "=", param.getSupplierId()).and("orderId", "in", orderIds)
.and("invoiceCode", "=", settlementId).and("invoiceStatus", "=", 2));
throw new IllegalArgumentException(invoice.getFailMsg());
}
dao().update(PointsMallOrderSub.class, Chain.make("invoiceStatus", 2), Cnd.where("supplierId", "=", param.getSupplierId()).and("orderId", "in", orderIds));
return "开票申请已生成,结算单号:" + settlementId; return "开票申请已生成,结算单号:" + settlementId;
} }
@@ -170,11 +454,7 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
} }
if (StrUtil.isNotBlank(param.getOrderId())) { if (StrUtil.isNotBlank(param.getOrderId())) {
List<String> orderIds = StrUtil.splitTrim(param.getOrderId(), ","); List<String> orderIds = StrUtil.splitTrim(param.getOrderId(), ",");
if (orderIds.size() > 1) {
cnd.and(field(alias, "orderId"), "in", orderIds); cnd.and(field(alias, "orderId"), "in", orderIds);
} else {
cnd.and(field(alias, "orderId"), "like", "%" + param.getOrderId() + "%");
}
} }
if (StrUtil.isNotBlank(param.getStartDate())) { if (StrUtil.isNotBlank(param.getStartDate())) {
cnd.and(field(alias, "orderCompleteTime"), ">=", DateUtil.beginOfDay(DateUtil.parse(param.getStartDate()))); cnd.and(field(alias, "orderCompleteTime"), ">=", DateUtil.beginOfDay(DateUtil.parse(param.getStartDate())));
@@ -196,16 +476,16 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
if (StrUtil.isBlank(param.getSupplierId())) { if (StrUtil.isBlank(param.getSupplierId())) {
throw new IllegalArgumentException("供应商不能为空"); throw new IllegalArgumentException("供应商不能为空");
} }
if (StrUtil.isBlank(param.getOrderIds())) {
throw new IllegalArgumentException("订单不能为空");
}
if (StrUtil.isBlank(param.getStartDate()) || StrUtil.isBlank(param.getEndDate())) { if (StrUtil.isBlank(param.getStartDate()) || StrUtil.isBlank(param.getEndDate())) {
throw new IllegalArgumentException("订单完成日期不能为空"); throw new IllegalArgumentException("订单完成日期不能为空");
} }
if (StrUtil.isBlank(param.getInvoiceDate())) { if (StrUtil.isBlank(param.getInvoiceDate())) {
throw new IllegalArgumentException("开票日期不能为空"); throw new IllegalArgumentException("开票日期不能为空");
} }
if (StrUtil.isBlank(param.getInvoiceCode())) { if (StrUtil.isBlank(param.getInvoiceTitle())) {
throw new IllegalArgumentException("发票抬头不能为空");
}
if (StrUtil.isBlank(param.getTaxIdNumber())) {
throw new IllegalArgumentException("纳税人识别号不能为空"); throw new IllegalArgumentException("纳税人识别号不能为空");
} }
if (StrUtil.isBlank(param.getInvoiceAddress())) { if (StrUtil.isBlank(param.getInvoiceAddress())) {
@@ -214,5 +494,8 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
if (StrUtil.isBlank(param.getInvoiceContact())) { if (StrUtil.isBlank(param.getInvoiceContact())) {
throw new IllegalArgumentException("联系电话不能为空"); throw new IllegalArgumentException("联系电话不能为空");
} }
if (param.getInvoiceType() == null) {
throw new IllegalArgumentException("发票类型不能为空");
}
} }
} }
@@ -1,5 +1,7 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.controller; package com.budwk.app.zhgh.pointsmall.mallbridge.controller;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.pointsmall.mallbridge.annotation.MallTokenRequired; import com.budwk.app.zhgh.pointsmall.mallbridge.annotation.MallTokenRequired;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.*; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.*;
@@ -13,6 +15,7 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.integration.jedis.RedisService;
import org.nutz.mvc.adaptor.JsonAdaptor; import org.nutz.mvc.adaptor.JsonAdaptor;
import org.nutz.mvc.annotation.AdaptBy; import org.nutz.mvc.annotation.AdaptBy;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
@@ -32,6 +35,25 @@ public class PointsMallBridgeController {
@Inject @Inject
private PointsMallBridgeService pointsMallBridgeService; private PointsMallBridgeService pointsMallBridgeService;
@Inject
private RedisService redisService;
@POST
@At("/connect/supplier-session")
@ApiOperation(value = "获取供应商用户会话", notes = "使用 H5 跳转 keyword 换取一次性用户会话")
public Result supplierSession(@Param("..") MallSupplierSessionDTO dto) {
if (dto == null || StrUtil.isBlank(dto.getKeyword())) {
return Result.error("keyword不能为空");
}
String redisKey = "supplier-session:" + dto.getKeyword();
String session = redisService.get(redisKey);
if (StrUtil.isBlank(session)) {
return Result.error("用户会话不存在或已失效");
}
redisService.del(redisKey);
return Result.success(JSONUtil.parseObj(session));
}
@POST @POST
@At("/connect/token") @At("/connect/token")
@ApiOperation(value = "获取 token", notes = "供应商获取访问令牌") @ApiOperation(value = "获取 token", notes = "供应商获取访问令牌")
@@ -7,6 +7,8 @@ import java.time.LocalDate;
@Data @Data
public class InvoiceInfoDTO { public class InvoiceInfoDTO {
private Integer status;
private String remark;
private String invoiceId; private String invoiceId;
private String invoiceCode; private String invoiceCode;
private LocalDate invoiceDate; private LocalDate invoiceDate;
@@ -14,7 +14,7 @@ public class MallInvoiceApplyDTO {
private BigDecimal orderTotalPrice; private BigDecimal orderTotalPrice;
private String invoiceDate; private String invoiceDate;
private String invoiceTitle; private String invoiceTitle;
private String invoiceCode; private String taxIdNumber;
private String invoiceAddress; private String invoiceAddress;
private String invoiceContact; private String invoiceContact;
private BigDecimal invoiceNakedPrice; private BigDecimal invoiceNakedPrice;
@@ -14,5 +14,8 @@ public class MallOrderEventDTO {
private String deliveryTime; private String deliveryTime;
private String logisticsOrderId; private String logisticsOrderId;
private String crrgBsnNm; private String crrgBsnNm;
// 接口推送时间,Unix 毫秒时间戳。
private Long time; private Long time;
// 本次业务操作发生时间,重推时保留原值,格式 yyyy-MM-dd HH:mm:ss。
private String operationTime;
} }
@@ -0,0 +1,24 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/** 订单消息数据,主单、子单和退货消息统一使用。 */
@Data
public class MallOrderMessageDTO {
private String supplierId;
private String userId;
private String userName;
// 退货消息使用退货单号,pointsPrice 使用本次退款积分。
private String orderId;
private BigDecimal pointsPrice;
private BigDecimal totalPrice;
private BigDecimal wPayOrAPay;
private Date orderCreateTime;
private Date orderFinishTime;
// 本次操作时间,取供应商事件的 operationTime,不使用接口推送时间或妥投时间。
private Date operationTime;
private String extJson;
}
@@ -0,0 +1,8 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.dto;
import lombok.Data;
@Data
public class MallSupplierSessionDTO {
private String keyword;
}
@@ -0,0 +1,44 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.dto;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class OrderInfoByReconciliationDTO {
/**
* 订单id
*/
private String orderId;
/**
* 供应商id
*/
private String supplierId;
/**
* 商品购买总价格(含税价)
*/
private BigDecimal totalPrice;
/**
* 订单消耗积分数,有退货需要计算
*/
private BigDecimal pointsPrice;
/**
* 订单微信支付或支付宝支付数目,有退货需要计算
*/
private BigDecimal wPayOrAPay;
/**
* 总价格(不含税)
*/
private BigDecimal invoiceNakedPrice;
/**
* 下单商品信息
*/
private List<OrderProInfoDTO> orderProInfos;
}
@@ -4,6 +4,7 @@ import lombok.Data;
@Data @Data
public class QueryInvoiceInfoDTO { public class QueryInvoiceInfoDTO {
private Boolean retry;
private String supplierId; private String supplierId;
private String token; private String token;
private String settlementId; private String settlementId;
@@ -0,0 +1,28 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.dto;
import lombok.Data;
@Data
public class QueryReconciliationInfoDTO {
private String supplierId;
private String token;
private Integer reconciliationType;
/**
* 订单ID
*/
private String orderId;
/**
* 格式为:yyyy-MM-dd(包含单日)
*/
private String startDate;
/**
* 格式为:yyyy-MM-dd(包含单日)
*/
private String endDate;
private Integer page;
private Integer pageSize;
private Long timestamp;
}
@@ -0,0 +1,15 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.dto;
import lombok.Data;
import java.util.List;
@Data
public class ReconciliationResultNoticeDTO {
private String supplierId;
private String token;
private Long timestamp;
private String reconciliationBatchNo;
private String reconciliationTime;
private List<ReconciliationResultOrderDTO> reconciliationResults;
}
@@ -0,0 +1,22 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.dto;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ReconciliationResultOrderDTO {
private String orderId;
private String userId;
private Integer reconciliationStatus;
private Integer diffType;
private String diffReason;
private BigDecimal platformTotalPrice;
private BigDecimal platformCashAmount;
private BigDecimal platformPointsPrice;
private BigDecimal platformRefundPrice;
private BigDecimal supplierTotalPrice;
private BigDecimal supplierCashAmount;
private BigDecimal supplierPointsPrice;
private BigDecimal supplierRefundPrice;
}
@@ -7,6 +7,7 @@ public class UnReconciledMsgNoticeDTO {
private String uniqueSeqNo; private String uniqueSeqNo;
private String supplierId; private String supplierId;
private String token; private String token;
private Long timestamp;
private String orderIds; private String orderIds;
private String unReconciledMsg; private String unReconciledMsg;
private String unReconciledType; private String unReconciledType;
@@ -3,7 +3,16 @@ package com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler; import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult; import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallInboundEventService; import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.trans.Atom;
import org.nutz.trans.Trans;
import java.util.Date;
import com.budwk.app.zhgh.pointsmall.points.service.PointsMallPointsLockService;
import java.math.BigDecimal;
import com.budwk.app.zhgh.pointsmall.order.enums.MallOrderStatusEnum; import com.budwk.app.zhgh.pointsmall.order.enums.MallOrderStatusEnum;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@@ -12,19 +21,44 @@ import org.nutz.ioc.loader.annotation.IocBean;
* MallAbolishSubOrderEventHandler * MallAbolishSubOrderEventHandler
* orderType=8: 废除子订单事件处理器。 * orderType=8: 废除子订单事件处理器。
*/ */
@IocBean @Slf4j
public class MallAbolishSubOrderEventHandler implements MallOrderEventHandler { @IocBean(args = {"refer:dao"})
public class MallAbolishSubOrderEventHandler extends AbstractMallBridgeSupport implements MallOrderEventHandler {
@Inject @Inject
private MallInboundEventService inboundEventService; private PointsMallPointsLockService pointsMallPointsLockService;
public MallAbolishSubOrderEventHandler(Dao dao) {
super(dao);
}
@Override @Override
public int getType() { public int getType() {
return 8; return 8;
} }
/**
* 废除指定子订单,在同一事务中更新状态并释放或退回积分。
*/
@Override @Override
public MallInboundResult handle(MallOrderEventDTO dto) { public MallInboundResult handle(MallOrderEventDTO dto) {
return inboundEventService.updateSubOrderStatus(dto, MallOrderStatusEnum.ABOLISH); Integer state = MallOrderStatusEnum.ABOLISH.getCode();
PointsMallOrderSub sub = dao.fetch(PointsMallOrderSub.class, Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "=", dto.getOrderId()).and("delFlag", "=", false));
if (sub == null) {
return MallInboundResult.fail(404, "订单不存在");
}
// 已废除的重复通知不再处理,避免重复执行积分释放或退回。
if (state.equals(sub.getOrderState())) {
log.info("积分商城子单状态已是目标状态,跳过重复处理,supplierId={}orderId={}state={}", dto.getSupplierId(), dto.getOrderId(), state);
return MallInboundResult.success();
}
Trans.exec((Atom) () -> {
sub.setOrderState(state);
sub.setOrderUpdateTime(new Date());
dao.updateIgnoreNull(sub);
});
log.info("积分商城子单状态更新完成,supplierId={}orderId={}state={}", dto.getSupplierId(), dto.getOrderId(), state);
return MallInboundResult.success();
} }
} }
@@ -3,7 +3,18 @@ package com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler; import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult; import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallInboundEventService; import cn.hutool.core.util.StrUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderMain;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub;
import com.budwk.app.zhgh.pointsmall.points.service.PointsMallPointsLockService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.trans.Atom;
import org.nutz.trans.Trans;
import java.util.Date;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@@ -11,19 +22,50 @@ import org.nutz.ioc.loader.annotation.IocBean;
* MallCancelPayedMainOrderEventHandler * MallCancelPayedMainOrderEventHandler
* orderType=7: 已支付未拆单的整个父单取消。 * orderType=7: 已支付未拆单的整个父单取消。
*/ */
@IocBean @Slf4j
public class MallCancelPayedMainOrderEventHandler implements MallOrderEventHandler { @IocBean(args = {"refer:dao"})
public class MallCancelPayedMainOrderEventHandler extends AbstractMallBridgeSupport implements MallOrderEventHandler {
@Inject @Inject
private MallInboundEventService inboundEventService; private PointsMallPointsLockService pointsMallPointsLockService;
public MallCancelPayedMainOrderEventHandler(Dao dao) {
super(dao);
}
@Override @Override
public int getType() { public int getType() {
return 7; return 7;
} }
/**
* 处理已支付未拆单主单取消通知,沿用原 paidNotSplit=true 分支的校验与积分处理。
*/
@Override @Override
public MallInboundResult handle(MallOrderEventDTO dto) { public MallInboundResult handle(MallOrderEventDTO dto) {
return inboundEventService.cancelMainOrder(dto, true); // 优先使用 mainOrderId,未提供时使用事件中的 orderId 查询主订单。
String mainOrderId = StrUtil.blankToDefault(dto.getMainOrderId(), dto.getOrderId());
if (StrUtil.isBlank(mainOrderId)) {
return MallInboundResult.fail(400, "主订单信息不能为空");
}
PointsMallOrderMain main = dao.fetch(PointsMallOrderMain.class, Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "=", mainOrderId).and("delFlag", "=", false));
if (main == null) {
return MallInboundResult.fail(404, "主订单不存在");
}
Date now = new Date();
// 主子订单状态更新与主单积分锁释放在同一事务内完成。
Trans.exec((Atom) () -> {
main.setOrderState(ORDER_CANCELLED);
main.setOrderUpdateTime(now);
dao.updateIgnoreNull(main);
// 同步取消该供应商、该主订单下尚未删除的子订单,保持原处理范围。
dao.update(PointsMallOrderSub.class, Chain.make("orderState", ORDER_CANCELLED).add("orderUpdateTime", now),
Cnd.where("supplierId", "=", dto.getSupplierId()).and("mainOrderId", "=", mainOrderId).and("delFlag", "=", false));
// 使用主订单归属信息释放主单积分锁。
pointsMallPointsLockService.releaseMain(main.getUserId(), main.getSupplierId(), main.getOrderId());
});
log.info("积分商城主单取消完成,supplierId={}mainOrderId={}paidNotSplit={}", dto.getSupplierId(), mainOrderId, true);
return MallInboundResult.success();
} }
} }
@@ -3,7 +3,18 @@ package com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler; import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult; import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallInboundEventService; import cn.hutool.core.util.StrUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderMain;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub;
import com.budwk.app.zhgh.pointsmall.points.service.PointsMallPointsLockService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.trans.Atom;
import org.nutz.trans.Trans;
import java.util.Date;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@@ -11,19 +22,54 @@ import org.nutz.ioc.loader.annotation.IocBean;
* MallCancelPendingOrderEventHandler * MallCancelPendingOrderEventHandler
* orderType=1: 取消待支付主单事件处理器。 * orderType=1: 取消待支付主单事件处理器。
*/ */
@IocBean @Slf4j
public class MallCancelPendingOrderEventHandler implements MallOrderEventHandler { @IocBean(args = {"refer:dao"})
public class MallCancelPendingOrderEventHandler extends AbstractMallBridgeSupport implements MallOrderEventHandler {
@Inject @Inject
private MallInboundEventService inboundEventService; private PointsMallPointsLockService pointsMallPointsLockService;
public MallCancelPendingOrderEventHandler(Dao dao) {
super(dao);
}
@Override @Override
public int getType() { public int getType() {
return 1; return 1;
} }
/**
* 处理待支付主单取消通知,校验待支付状态后取消订单并释放主单积分锁。
*/
@Override @Override
public MallInboundResult handle(MallOrderEventDTO dto) { public MallInboundResult handle(MallOrderEventDTO dto) {
return inboundEventService.cancelMainOrder(dto, false); // 优先使用 mainOrderId,未提供时使用事件中的 orderId 查询主订单。
String mainOrderId = StrUtil.blankToDefault(dto.getMainOrderId(), dto.getOrderId());
if (StrUtil.isBlank(mainOrderId)) {
return MallInboundResult.fail(400, "主订单信息不能为空");
}
PointsMallOrderMain main = dao.fetch(PointsMallOrderMain.class, Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "=", mainOrderId).and("delFlag", "=", false));
if (main == null) {
return MallInboundResult.fail(404, "主订单不存在");
}
// 待支付取消事件仅允许取消尚未支付的主订单。
if (!Integer.valueOf(ORDER_PENDING_PAY).equals(main.getOrderState())) {
return MallInboundResult.fail(400, "只能取消未支付主单");
}
Date now = new Date();
// 主子订单状态更新与主单积分锁释放在同一事务内完成。
Trans.exec((Atom) () -> {
main.setOrderState(ORDER_CANCELLED);
main.setOrderUpdateTime(now);
dao.updateIgnoreNull(main);
// 同步取消该供应商、该主订单下尚未删除的子订单,保持原处理范围。
dao.update(PointsMallOrderSub.class, Chain.make("orderState", ORDER_CANCELLED).add("orderUpdateTime", now),
Cnd.where("supplierId", "=", dto.getSupplierId()).and("mainOrderId", "=", mainOrderId).and("delFlag", "=", false));
// 使用主订单归属信息释放主单积分锁。
pointsMallPointsLockService.releaseMain(main.getUserId(), main.getSupplierId(), main.getOrderId());
});
log.info("积分商城主单取消完成,supplierId={}mainOrderId={}paidNotSplit={}", dto.getSupplierId(), mainOrderId, false);
return MallInboundResult.success();
} }
} }
@@ -1,6 +1,8 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers; package com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderMessageDTO;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.sys.models.Sys_user; import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO;
@@ -61,7 +63,7 @@ public class MallCancelSubOrderEventHandler implements MallOrderEventHandler {
if (dto == null || StrUtil.hasBlank(dto.getSupplierId(), dto.getOrderId(), dto.getUserId())) { if (dto == null || StrUtil.hasBlank(dto.getSupplierId(), dto.getOrderId(), dto.getUserId())) {
return MallInboundResult.fail(400, "供应商、订单、用户不能为空"); return MallInboundResult.fail(400, "供应商、订单、用户不能为空");
} }
// userId 兼容系统用户ID和工号,后续消息推送需要使用用户信息 // 供应商传入 userId 为虚拟用户ID,转换为系统用户后用于消息推送
Sys_user user = fetchUser(dto.getUserId()); Sys_user user = fetchUser(dto.getUserId());
if (user == null) { if (user == null) {
return MallInboundResult.fail(-1, "用户不存在"); return MallInboundResult.fail(-1, "用户不存在");
@@ -102,7 +104,10 @@ public class MallCancelSubOrderEventHandler implements MallOrderEventHandler {
pointsMallOrderService.updateMainOrderStatus(sub.getMainOrderId(), sub.getSupplierId()); pointsMallOrderService.updateMainOrderStatus(sub.getMainOrderId(), sub.getSupplierId());
}); });
try { try {
mallCallPMessagePushService.pushMessageToGlobal(StrUtil.blankToDefault(user.getUsername(), user.getLoginname()), sub); MallOrderMessageDTO message = BeanUtil.copyProperties(sub, MallOrderMessageDTO.class);
message.setOperationTime(StrUtil.isBlank(dto.getOperationTime()) ? null : DateUtil.parse(dto.getOperationTime().replace("T", " ")));
message.setUserName(StrUtil.blankToDefault(user.getUsername(), user.getLoginname()));
mallCallPMessagePushService.pushMessageToGlobal("66666666", message);
} catch (Exception e) { } catch (Exception e) {
log.error("9.2.1 子单取消,callP消息推送:{},{}", dto, e.getMessage()); log.error("9.2.1 子单取消,callP消息推送:{},{}", dto, e.getMessage());
} }
@@ -110,14 +115,8 @@ public class MallCancelSubOrderEventHandler implements MallOrderEventHandler {
return MallInboundResult.success(); return MallInboundResult.success();
} }
/** private Sys_user fetchUser(String virtualId) {
* 入参 userId 可能是系统用户ID或工号,按当前项目用户表兼容查询。 return dao.fetch(Sys_user.class, Cnd.where("virtual_id", "=", virtualId)
*/ .and("disabled", "=", false).and("delFlag", "=", false));
private Sys_user fetchUser(String userId) {
Sys_user user = dao.fetch(Sys_user.class, Cnd.where("id", "=", userId).and("disabled", "=", false).and("delFlag", "=", false));
if (user != null) {
return user;
}
return dao.fetch(Sys_user.class, Cnd.where("loginname", "=", userId).and("disabled", "=", false).and("delFlag", "=", false));
} }
} }
@@ -1,9 +1,26 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers; package com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderMessageDTO;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler; import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult; import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallInboundEventService; import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.trans.Atom;
import org.nutz.trans.Trans;
import java.util.Date;
import cn.hutool.json.JSONUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderQueryDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.OrderInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallOutboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallOrderQueryOutboundService;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallCallPMessagePushService;
import com.budwk.app.zhgh.pointsmall.order.enums.MallOrderStatusEnum; import com.budwk.app.zhgh.pointsmall.order.enums.MallOrderStatusEnum;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@@ -16,19 +33,103 @@ import org.nutz.ioc.loader.annotation.IocBean;
* <p>注意:供应商侧“确认收货”对应 zhgh/callP 侧“已妥投(4)”, * <p>注意:供应商侧“确认收货”对应 zhgh/callP 侧“已妥投(4)”,
* 不等同于 zhgh/callP 用户确认收货后的“已完成(5)”。</p> * 不等同于 zhgh/callP 用户确认收货后的“已完成(5)”。</p>
*/ */
@IocBean @Slf4j
public class MallConfirmReceiptEventHandler implements MallOrderEventHandler { @IocBean(args = {"refer:dao"})
public class MallConfirmReceiptEventHandler extends AbstractMallBridgeSupport implements MallOrderEventHandler {
@Inject @Inject
private MallInboundEventService inboundEventService; private MallOrderQueryOutboundService mallOrderQueryOutboundService;
@Inject
private MallCallPMessagePushService mallCallPMessagePushService;
public MallConfirmReceiptEventHandler(Dao dao) {
super(dao);
}
@Override @Override
public int getType() { public int getType() {
return 6; return 6;
} }
/**
* 处理供应商确认收货通知,仅更新为已妥投,不执行用户确认完成后的积分扣减。
*/
@Override @Override
public MallInboundResult handle(MallOrderEventDTO dto) { public MallInboundResult handle(MallOrderEventDTO dto) {
return inboundEventService.updateSubOrderStatus(dto, MallOrderStatusEnum.RECEIVED); Integer state = MallOrderStatusEnum.RECEIVED.getCode();
PointsMallOrderSub sub = dao.fetch(PointsMallOrderSub.class, Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "=", dto.getOrderId()).and("delFlag", "=", false));
if (sub == null) {
return MallInboundResult.fail(404, "订单不存在");
}
// 已妥投的重复通知直接返回,保留原处理的幂等行为。
if (state.equals(sub.getOrderState())) {
log.info("积分商城子单状态已是目标状态,跳过重复处理,supplierId={}orderId={}state={}", dto.getSupplierId(), dto.getOrderId(), state);
return MallInboundResult.success();
}
Trans.exec((Atom) () -> {
// 补查供应商订单详情;查询失败时沿用原逻辑,仍更新本地妥投状态。
fillReceivedSubOrder(dto, sub);
sub.setOrderState(state);
// 优先保留订单详情中的时间,缺失时使用当前时间。
if (sub.getOrderUpdateTime() == null) {
sub.setOrderUpdateTime(new Date());
}
if (sub.getOrderFinishTime() == null) {
sub.setOrderFinishTime(new Date());
}
dao.updateIgnoreNull(sub);
});
// 妥投状态保存成功后发送提醒,消息异常不回滚订单处理。
try {
// 按子订单所属用户读取姓名,显式传入妥投提醒的 userName 模板变量。
Sys_user user = dao.fetch(Sys_user.class, Cnd.where("id", "=", sub.getUserId()).or("loginname", "=", sub.getUserId()));
if (user == null) {
throw new IllegalArgumentException("订单所属用户不存在,无法生成妥投提醒");
}
MallOrderMessageDTO message = BeanUtil.copyProperties(sub, MallOrderMessageDTO.class);
message.setOperationTime(parseDate(dto.getOperationTime()));
message.setUserName(StrUtil.blankToDefault(user.getUsername(), user.getLoginname()));
mallCallPMessagePushService.pushMessageToGlobal("22222222", message);
} catch (Exception e) {
log.error("订单类型为:6(确认收货)时,全局消息推送失败:{}", dto, e);
}
log.info("积分商城子单状态更新完成,supplierId={}orderId={}state={}", dto.getSupplierId(), dto.getOrderId(), state);
return MallInboundResult.success();
}
/**
* 订单类型为:6(确认收货)时,调用 9.2.2 获取订单详情接口,补充子单妥投相关信息。
*
* <p>供应商侧确认收货只代表电商处已收货,对 zhgh/callP 侧应更新为已妥投(4)
* 等待 callP 用户侧确认收货后才进入已完成(5)。</p>
*/
private void fillReceivedSubOrder(MallOrderEventDTO dto, PointsMallOrderSub sub) {
if (mallOrderQueryOutboundService == null) {
return;
}
MallOrderQueryDTO queryDTO = new MallOrderQueryDTO();
queryDTO.setSupplierId(dto.getSupplierId());
queryDTO.setOrderId(dto.getOrderId());
queryDTO.setOrderType(2);
log.info("订单类型为:6(确认收货)时,调用9.2.2获取订单详情接口:{}", queryDTO);
try {
MallOutboundResult result = mallOrderQueryOutboundService.queryOrderInfo(queryDTO);
if (result == null || result.getResultCode() != 0 || result.getOrderInfo() == null || result.getOrderInfo().isEmpty()) {
log.error("订单类型为:6(确认收货)时,调用9.2.2 获取订单详情失败:{},{}", queryDTO, result == null ? null : result.getMessage());
return;
}
OrderInfoDTO orderInfo = result.getOrderInfo().get(0);
sub.setOrderCreateTime(parseDate(orderInfo.getCreateTime()));
sub.setOrderUpdateTime(parseDate(orderInfo.getUpdateTime()));
sub.setOrderFinishTime(parseDate(orderInfo.getOrderFinishTime()));
sub.setDeliveryTime(parseDate(orderInfo.getDeliveryTime()));
sub.setDeliveryStatus(parseInteger(orderInfo.getDeliveryStatus()));
if (orderInfo.getOrderProInfos() != null) {
sub.setExtJson(JSONUtil.toJsonStr(orderInfo.getOrderProInfos()));
}
} catch (Exception e) {
log.error("订单类型为:6(确认收货)时,调用9.2.2 获取订单详情异常:{},{}", queryDTO, e.getMessage());
}
} }
} }
@@ -3,7 +3,7 @@ package com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler; import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult; import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallInboundEventService; import com.budwk.app.zhgh.pointsmall.invoice.service.PointsMallInvoiceService;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@@ -15,7 +15,7 @@ import org.nutz.ioc.loader.annotation.IocBean;
public class MallInvoiceInfoEventHandler implements MallOrderEventHandler { public class MallInvoiceInfoEventHandler implements MallOrderEventHandler {
@Inject @Inject
private MallInboundEventService inboundEventService; private PointsMallInvoiceService pointsMallInvoiceService;
@Override @Override
public int getType() { public int getType() {
@@ -24,6 +24,6 @@ public class MallInvoiceInfoEventHandler implements MallOrderEventHandler {
@Override @Override
public MallInboundResult handle(MallOrderEventDTO dto) { public MallInboundResult handle(MallOrderEventDTO dto) {
return inboundEventService.invoiceInfo(dto); return pointsMallInvoiceService.invoiceInfo(dto);
} }
} }
@@ -3,27 +3,116 @@ package com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler; import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult; import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallInboundEventService; import cn.hutool.core.util.StrUtil;
import org.nutz.ioc.loader.annotation.Inject; import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderMain;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.trans.Atom;
import org.nutz.trans.Trans;
import java.util.Date;
import java.util.List;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
/** /**
* MallShipOrderEventHandler * MallShipOrderEventHandler
* orderType=99: 供应商已发货 / 更新发货状态。 * orderType=99: 供应商已发货 / 更新发货状态。
* 在处理器内完成订单校验及主子订单物流信息更新,不再回调事件分发服务。
* 本事件仅更新发货状态和物流信息,不修改订单业务状态 orderState。
*/ */
@IocBean @Slf4j
public class MallShipOrderEventHandler implements MallOrderEventHandler { @IocBean(args = {"refer:dao"})
public class MallShipOrderEventHandler extends AbstractMallBridgeSupport implements MallOrderEventHandler {
@Inject public MallShipOrderEventHandler(Dao dao) {
private MallInboundEventService inboundEventService; super(dao);
}
@Override @Override
public int getType() { public int getType() {
return 99; return 99;
} }
/**
* 处理供应商发货通知,在同一事务中更新匹配的子订单及一个主订单。
*
* @param dto 发货事件,orderId 为英文逗号分隔的子订单号,userId 为可选的用户虚拟 ID
* @return 参数或用户映射无效时返回 400,无匹配子订单时返回 404,更新完成返回成功
*/
@Override @Override
public MallInboundResult handle(MallOrderEventDTO dto) { public MallInboundResult handle(MallOrderEventDTO dto) {
return inboundEventService.updateDeliveryStatus(dto); if (StrUtil.isBlank(dto.getSupplierId()) || StrUtil.isBlank(dto.getOrderId())) {
return MallInboundResult.fail(400, "供应商或订单不能为空");
}
// 按供应商和明确的子订单号限定更新范围,支持一次通知多个子订单。
List<String> orderIds = splitIds(dto.getOrderId());
if (orderIds.isEmpty()) {
return MallInboundResult.fail(400, "订单信息不能为空");
}
// 未提供有效发货时间时,使用当前处理时间。
Date deliveryTime = parseDate(dto.getDeliveryTime());
if (deliveryTime == null) {
deliveryTime = new Date();
}
Cnd subCnd = Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "in", orderIds)
.and("delFlag", "=", false);
if (StrUtil.isNotBlank(dto.getUserId())) {
// 供应商传入虚拟 ID,转换为本地用户 ID 后参与订单归属查询。
Sys_user user = fetchUser(dto.getUserId());
if (user == null) {
return MallInboundResult.fail(400, "用户ID未配置或不存在");
}
subCnd.and("userId", "=", user.getId());
}
List<PointsMallOrderSub> subs = dao.query(PointsMallOrderSub.class, subCnd);
if (subs.isEmpty()) {
return MallInboundResult.fail(404, "订单不存在");
}
Date finalDeliveryTime = deliveryTime;
// 已发货事件固定写入 deliveryStatus=1,不使用通知中的状态值覆盖该语义。
Integer deliveredStatus = 1;
Trans.exec((Atom) () -> {
// 物流单号和承运商仅在非空时更新,避免空字段覆盖已有物流信息。
Chain subChain = Chain.make("deliveryStatus", deliveredStatus)
.add("deliveryTime", finalDeliveryTime);
if (StrUtil.isNotBlank(dto.getLogisticsOrderId())) {
subChain.add("logisticsOrderId", dto.getLogisticsOrderId());
}
if (StrUtil.isNotBlank(dto.getCrrgBsnNm())) {
subChain.add("crrgBsnNm", dto.getCrrgBsnNm());
}
dao.update(PointsMallOrderSub.class, subChain, subCnd);
// 优先使用通知中的主订单号,未提供时取首个匹配子订单的主订单号。
// 此处只同步该主订单的物流信息,不检查其余子订单是否全部发货。
String mainOrderId = StrUtil.blankToDefault(dto.getMainOrderId(), subs.get(0).getMainOrderId());
if (StrUtil.isNotBlank(mainOrderId)) {
// 主订单的发货状态字段为字符串,子订单字段为整数。
Chain mainChain = Chain.make("deliveryStatus", String.valueOf(deliveredStatus))
.add("deliveryTime", finalDeliveryTime);
if (StrUtil.isNotBlank(dto.getLogisticsOrderId())) {
mainChain.add("logisticsOrderId", dto.getLogisticsOrderId());
}
if (StrUtil.isNotBlank(dto.getCrrgBsnNm())) {
mainChain.add("crrgBsnNm", dto.getCrrgBsnNm());
}
dao.update(PointsMallOrderMain.class, mainChain, Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "=", mainOrderId).and("delFlag", "=", false));
}
});
log.info("供应商已发货事件处理完成,supplierId={}mainOrderId={}orderIds={}deliveryStatus={}",
dto.getSupplierId(), dto.getMainOrderId(), orderIds, deliveredStatus);
return MallInboundResult.success();
} }
} }
@@ -94,7 +94,7 @@ public class MallSplitSubOrderEventHandler implements MallOrderEventHandler {
// 调用外部服务获取子订单信息 // 调用外部服务获取子订单信息
MallOutboundResult result = mallOrderQueryOutboundService.orderQuery(queryDTO); MallOutboundResult result = mallOrderQueryOutboundService.orderQuery(queryDTO);
if (result == null || result.getResultCode() != 0) { if (result == null || result.getResultCode() != 0) {
throw new RuntimeException(result == null ? "获取订单详情返回为空" : result.getMessage()); return MallInboundResult.fail(result == null ? 502 : result.getResultCode(), result == null ? "获取订单详情返回为空" : result.getMessage());
} }
orderInfoList = result.getOrderInfo(); orderInfoList = result.getOrderInfo();
} catch (Exception e) { } catch (Exception e) {
@@ -1,4 +1,5 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers; package com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderMessageDTO;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
@@ -148,13 +149,15 @@ public class MallSubOrderReturnEventHandler extends AbstractMallBridgeSupport im
}); });
try { try {
// callP消息推送 // callP消息推送
PointsMallOrderSub notifySub = BeanUtil.copyProperties(sub, PointsMallOrderSub.class); MallOrderMessageDTO notifySub = BeanUtil.copyProperties(sub, MallOrderMessageDTO.class);
notifySub.setOperationTime(parseDate(dto.getOperationTime()));
notifySub.setOrderId(dto.getOrderId()); notifySub.setOrderId(dto.getOrderId());
notifySub.setPointsPrice(refund); notifySub.setPointsPrice(refund);
notifySub.setTotalPrice(orderInfo.getTotalPrice()); notifySub.setTotalPrice(orderInfo.getTotalPrice());
notifySub.setWPayOrAPay(orderInfo.getWPayOrAPay()); notifySub.setWPayOrAPay(orderInfo.getWPayOrAPay());
notifySub.setExtJson(orderExchange.getExtJson()); notifySub.setExtJson(orderExchange.getExtJson());
mallCallPMessagePushService.pushMessageToGlobal(StrUtil.blankToDefault(user.getUsername(), user.getLoginname()), notifySub); notifySub.setUserName(StrUtil.blankToDefault(user.getUsername(), user.getLoginname()));
mallCallPMessagePushService.pushMessageToGlobal("66666666", notifySub);
} catch (Exception e) { } catch (Exception e) {
log.error("9.2.1 子单取消 或 退货,callP消息推送: {}, {}", dto, e.getMessage()); log.error("9.2.1 子单取消 或 退货,callP消息推送: {}, {}", dto, e.getMessage());
} }
@@ -0,0 +1,55 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.model;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.OrderInfoByReconciliationDTO;
import lombok.Data;
import java.util.List;
@Data
public class MallOutboundPageResult {
/**
* 0: 查询成功, 其他: 响应失败错误码
*/
private int resultCode;
/**
* 失败时, 返回失败原因
*/
private String message;
/**
* 总页
*/
private Integer totalPage;
/**
* 当前页
*/
private Integer currentPage;
//9.2.2 获取订单详情接口时不能为空
private List<OrderInfoByReconciliationDTO> orderInfos;
public static MallOutboundPageResult success() {
MallOutboundPageResult r = new MallOutboundPageResult();
r.resultCode = 0;
r.message = "成功";
r.orderInfos = null;
return r;
}
public static MallOutboundPageResult success(List<OrderInfoByReconciliationDTO> o) {
MallOutboundPageResult r = new MallOutboundPageResult();
r.resultCode = 0;
r.message = "成功";
r.orderInfos = o;
return r;
}
public static MallOutboundPageResult fail(int code, String message) {
MallOutboundPageResult r = new MallOutboundPageResult();
r.resultCode = code;
r.message = message;
return r;
}
}
@@ -8,8 +8,7 @@ import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.OrderProInfoDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.OrderProInfoDTO;
import com.budwk.app.zhgh.pointsmall.message.models.PointsMallMessage; import com.budwk.app.zhgh.pointsmall.message.models.PointsMallMessage;
import com.budwk.app.zhgh.pointsmall.message.models.PointsMallMessageTemplate; import com.budwk.app.zhgh.pointsmall.message.models.PointsMallMessageTemplate;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderMain; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderMessageDTO;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier; import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
@@ -39,123 +38,111 @@ public class MallCallPMessagePushService {
} }
/** /**
* 支付成功callP消息推送 * 统一订单消息入口:11111111 支付成功,22222222 妥投提醒,66666666 积分退回
* * 主单、子单均转换为 MallOrderMessageDTO,退货传入退货单号及本次退款积分。
* @param mainOrder 订单信息 * 已成功消息不重复发送;失败记录可在再次调用时重用。
* synchronized 防止当前服务实例内的并发通知重复创建消息。
*/ */
public void pushMessageToGlobal(PointsMallOrderMain mainOrder) { public synchronized void pushMessageToGlobal(String templateId, MallOrderMessageDTO order) {
PointsMallMessageTemplate template = dao.fetch(PointsMallMessageTemplate.class, Cnd.where("id", "=", "11111111").and("delFlag", "=", false)); if (order == null || StrUtil.hasBlank(templateId, order.getSupplierId(), order.getUserId(), order.getOrderId())) {
if (template == null) { throw new IllegalArgumentException("消息模板、供应商、用户和订单不能为空");
log.info("支付成功推送消息失败,未配置消息模板"); }
if ("66666666".equals(templateId) && nvl(order.getPointsPrice()).signum() <= 0) {
return; return;
} }
Sys_user user = dao.fetch(Sys_user.class, Cnd.where("id", "=", mainOrder.getUserId()).or("loginname", "=", mainOrder.getUserId())); String userId = order.getUserId();
PointsMallSupplier supplier = dao.fetch(PointsMallSupplier.class, Cnd.where("supplierId", "=", mainOrder.getSupplierId()).and("delFlag", "=", false));
String supplierName = supplier == null || StrUtil.isBlank(supplier.getSupplierName()) ? mainOrder.getSupplierId() : supplier.getSupplierName();
// 消息唯一标识 模板id+供应商+订单号
String uniqueNo = template.getId().concat("-").concat(mainOrder.getSupplierId()).concat("-").concat(mainOrder.getOrderId());
int count = dao.count(PointsMallMessage.class, Cnd.where("uniqueNo", "=", uniqueNo).and("delFlag", "=", false));
if (count > 0) {
log.info("9.2.14支付结果通知接口-支付成功2,消息重复推送:{}", uniqueNo);
return;
}
StringBuilder sb = new StringBuilder();
sb.append(" 商品列表:").append(System.lineSeparator());
List<OrderProInfoDTO> productList = StrUtil.isBlank(mainOrder.getExtJson())
? Collections.emptyList()
: JSONUtil.toList(mainOrder.getExtJson(), OrderProInfoDTO.class);
productList.forEach(product ->
sb.append(" 商品名称:").append(product.getName()).append(",商品价格:").append(formatMoney(product.getPrice())).append("").append(System.lineSeparator())
);
// 模板变量替换数据
Map<String, String> paramMap = new HashMap<>();
paramMap.put("#{userName}", user == null || StrUtil.isBlank(user.getUsername()) ? mainOrder.getUserId() : user.getUsername());
paramMap.put("#{orderFinishTime}", DateUtil.formatDateTime(mainOrder.getOrderCreateTime()));
paramMap.put("#{pointPrice}", nvl(mainOrder.getPointsPrice()).toPlainString());
paramMap.put("#{supplierName}", supplierName);
paramMap.put("#{totalPrice}", nvl(mainOrder.getTotalPrice()).toPlainString());
paramMap.put("#{payPrice}", nvl(mainOrder.getWPayOrAPay()).toPlainString());
paramMap.put("#{productList}", sb.toString());
// 模板转换成具体的消息
String content = template.getContent();
for (Map.Entry<String, String> entry : paramMap.entrySet()) {
content = content.replace(entry.getKey(), entry.getValue());
}
// 生成记录
PointsMallMessage message = new PointsMallMessage();
message.setUniqueNo(uniqueNo);
message.setPushType("CALLP");
message.setTemplateId(template.getId());
message.setTouser(mainOrder.getUserId());
message.setContent(content);
message.setContentLink(template.getContentLink());
message.setPlanPushTime(new Date());
// 状态设置为已推送
message.setStatus(1);
dao.insert(message);
globalMessageSendService.sendLocalSystemMessage(template.getName(), content, 1, Collections.singletonList(mainOrder.getUserId()), null);
}
/**
* callP消息推送。
*
* @param userName 用户姓名
* @param subOrder 订单信息
*/
public void pushMessageToGlobal(String userName, PointsMallOrderSub subOrder) {
PointsMallMessageTemplate template = dao.fetch(PointsMallMessageTemplate.class, Cnd.where("id", "=", "22222222").and("delFlag", "=", false));
if (template == null) {
log.info("子单取消后推送消息失败,未配置消息模板");
return;
}
PointsMallSupplier supplier = dao.fetch(PointsMallSupplier.class, Cnd.where("supplierId", "=", subOrder.getSupplierId()).and("delFlag", "=", false));
String supplierName = supplier == null || StrUtil.isBlank(supplier.getSupplierName()) ? subOrder.getSupplierId() : supplier.getSupplierName();
Date now = new Date(); Date now = new Date();
Date finishTime = subOrder.getOrderFinishTime() == null ? now : subOrder.getOrderFinishTime(); Date finishTime = "11111111".equals(templateId) ? order.getOrderCreateTime()
long day = DateUtil.betweenDay(DateUtil.beginOfDay(finishTime), DateUtil.beginOfDay(now), false); : order.getOrderFinishTime() == null ? now : order.getOrderFinishTime();
String uniqueNo = template.getId().concat("-").concat(subOrder.getSupplierId()).concat("-") Date planPushTime = now;
.concat(subOrder.getOrderId()).concat("-").concat(String.valueOf(day)); String uniqueNo = templateId + "-" + order.getSupplierId() + "-" + order.getOrderId();
if ("22222222".equals(templateId)) {
StringBuilder sb = new StringBuilder(); // 妥投提醒按自然日去重;取消、退货按业务订单号去重。
sb.append("商品列表: ").append(System.lineSeparator()); if (DateUtil.beginOfDay(finishTime).after(DateUtil.beginOfDay(now))) {
List<OrderProInfoDTO> productList = StrUtil.isBlank(subOrder.getExtJson()) return;
? Collections.emptyList()
: JSONUtil.toList(subOrder.getExtJson(), OrderProInfoDTO.class);
productList.forEach(product ->
sb.append(" 商品名称: ").append(product.getName()).append(",商品价格:").append(formatMoney(product.getPrice())).append("").append(System.lineSeparator())
);
Map<String, String> paramMap = new HashMap<>();
paramMap.put("#{userName}", userName);
paramMap.put("#{orderFinishTime}", DateUtil.formatDateTime(finishTime));
paramMap.put("#{pointPrice}", nvl(subOrder.getPointsPrice()).toPlainString());
paramMap.put("#{supplierName}", supplierName);
paramMap.put("#{totalPrice}", nvl(subOrder.getTotalPrice()).toPlainString());
paramMap.put("#{payPrice}", nvl(subOrder.getWPayOrAPay()).toPlainString());
paramMap.put("#{productList}", sb.toString());
String content = template.getContent();
for (Map.Entry<String, String> entry : paramMap.entrySet()) {
content = content.replace(entry.getKey(), entry.getValue());
} }
long day = DateUtil.betweenDay(DateUtil.beginOfDay(finishTime), DateUtil.beginOfDay(now), false);
Date planPushTime = DateUtil.offsetDay(finishTime, (int) day); uniqueNo += "-" + day;
PointsMallMessage message = new PointsMallMessage(); planPushTime = DateUtil.offsetDay(finishTime, (int) day);
}
PointsMallMessageTemplate template = dao.fetch(PointsMallMessageTemplate.class,
Cnd.where("id", "=", templateId).and("delFlag", "=", false));
if (template == null || StrUtil.isBlank(template.getContent())) {
log.info("订单消息推送失败,未配置消息模板或模板内容为空,templateId={}", templateId);
return;
}
PointsMallMessage message = dao.fetch(PointsMallMessage.class,
Cnd.where("uniqueNo", "=", uniqueNo).and("delFlag", "=", false));
if (message != null && Integer.valueOf(1).equals(message.getStatus())) {
log.info("订单消息已推送,跳过重复通知:{}", uniqueNo);
return;
}
Map<String, String> params = orderParams(order.getSupplierId(), userId, order.getExtJson(),
"11111111".equals(templateId), order.getTotalPrice());
if (StrUtil.isNotBlank(order.getUserName())) {
params.put("#{userName}", order.getUserName());
}
params.put("#{orderId}", order.getOrderId());
params.put("#{orderFinishTime}", finishTime == null ? "" : DateUtil.formatDateTime(finishTime));
params.put("#{operationTime}", order.getOperationTime() == null ? "" : DateUtil.formatDateTime(order.getOperationTime()));
params.put("#{pointPrice}", nvl(order.getPointsPrice()).toPlainString());
params.put("#{totalPrice}", nvl(order.getTotalPrice()).toPlainString());
params.put("#{payPrice}", nvl(order.getWPayOrAPay()).toPlainString());
String content = template.getContent();
for (Map.Entry<String, String> entry : params.entrySet()) {
content = content.replace(entry.getKey(), StrUtil.blankToDefault(entry.getValue(), ""));
}
boolean create = message == null;
if (create) {
message = new PointsMallMessage();
message.setUniqueNo(uniqueNo); message.setUniqueNo(uniqueNo);
}
message.setPushType("CALLP"); message.setPushType("CALLP");
message.setTemplateId(template.getId()); message.setTemplateId(templateId);
message.setTouser(subOrder.getUserId()); message.setTouser(userId);
message.setContent(content); message.setContent(content);
message.setContentLink(template.getContentLink()); message.setContentLink(template.getContentLink());
message.setPlanPushTime(planPushTime); message.setPlanPushTime(planPushTime);
message.setStatus(0); message.setStatus(0);
if (create) {
dao.insert(message); dao.insert(message);
} else {
dao.updateIgnoreNull(message);
}
try {
globalMessageSendService.sendLocalSystemMessage(template.getName(), content, 1, Collections.singletonList(userId), null);
message.setStatus(1);
dao.updateIgnoreNull(message);
} catch (Exception e) {
message.setStatus(2);
dao.updateIgnoreNull(message);
throw new IllegalStateException("订单消息推送失败:" + uniqueNo, e);
}
}
globalMessageSendService.sendLocalSystemMessage(template.getName(), content, 1, Collections.singletonList(subOrder.getUserId()), null); /** 公共模板变量:用户姓名、供应商名称和订单商品列表。 */
private Map<String, String> orderParams(String supplierId, String userId, String extJson,
boolean paymentMessage, BigDecimal totalPrice) {
Sys_user user = dao.fetch(Sys_user.class, Cnd.where("id", "=", userId).or("loginname", "=", userId));
PointsMallSupplier supplier = dao.fetch(PointsMallSupplier.class, Cnd.where("supplierId", "=", supplierId).and("delFlag", "=", false));
Map<String, String> params = new HashMap<>();
params.put("#{userName}", user == null ? userId : StrUtil.blankToDefault(user.getUsername(), userId));
params.put("#{supplierName}", supplier == null ? supplierId : StrUtil.blankToDefault(supplier.getSupplierName(), supplierId));
StringBuilder sb = new StringBuilder();
sb.append(paymentMessage ? "商品列表:" : "商品列表: ").append(System.lineSeparator());
List<OrderProInfoDTO> products = StrUtil.isBlank(extJson) || "null".equalsIgnoreCase(extJson.trim())
? Collections.emptyList() : JSONUtil.toList(extJson, OrderProInfoDTO.class);
if (paymentMessage) {
products.forEach(product -> sb.append("商品名称:").append(product.getName())
.append(" * ").append(product.getNumber() == null ? "-" : product.getNumber())
.append(",商品单价:").append(formatMoney(product.getPrice())).append("").append(System.lineSeparator()));
sb.append(System.lineSeparator()).append("订单合计:¥").append(formatMoney(totalPrice));
} else {
products.forEach(product -> sb.append(" 商品名称: ").append(product.getName())
.append(",商品价格:").append(formatMoney(product.getPrice())).append("").append(System.lineSeparator()));
}
params.put("#{productList}", sb.toString());
return params;
} }
private String formatMoney(BigDecimal value) { private String formatMoney(BigDecimal value) {
@@ -1,11 +1,6 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.service; package com.budwk.app.zhgh.pointsmall.mallbridge.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderQueryDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.OrderInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.OrderProInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler; import com.budwk.app.zhgh.pointsmall.mallbridge.event.MallOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers.MallAbolishSubOrderEventHandler; import com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers.MallAbolishSubOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers.MallCancelPayedMainOrderEventHandler; import com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers.MallCancelPayedMainOrderEventHandler;
@@ -18,28 +13,15 @@ import com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers.MallShipOrderEven
import com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers.MallSplitSubOrderEventHandler; import com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers.MallSplitSubOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers.MallSubOrderReturnEventHandler; import com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers.MallSubOrderReturnEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult; import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallOutboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport; import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport;
import com.budwk.app.zhgh.pointsmall.order.enums.MallOrderStatusEnum;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderMain;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub;
import com.budwk.app.zhgh.pointsmall.points.service.PointsMallPointsLockService; import com.budwk.app.zhgh.pointsmall.points.service.PointsMallPointsLockService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.trans.Atom;
import org.nutz.trans.Trans;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import com.budwk.app.sys.services.SysFileService;
/** /**
* 商城桥接订单事件入站服务。 * 商城桥接订单事件入站服务。
@@ -53,6 +35,10 @@ public class MallInboundEventService extends AbstractMallBridgeSupport {
@Inject @Inject
private MallOrderQueryOutboundService mallOrderQueryOutboundService; private MallOrderQueryOutboundService mallOrderQueryOutboundService;
@Inject @Inject
private MallInvoiceInfoQueryService mallInvoiceInfoQueryService;
@Inject
private SysFileService sysFileService;
@Inject
private MallCancelPendingOrderEventHandler mallCancelPendingOrderEventHandler; private MallCancelPendingOrderEventHandler mallCancelPendingOrderEventHandler;
@Inject @Inject
private MallSplitSubOrderEventHandler mallSplitSubOrderEventHandler; private MallSplitSubOrderEventHandler mallSplitSubOrderEventHandler;
@@ -117,307 +103,4 @@ public class MallInboundEventService extends AbstractMallBridgeSupport {
handlerRegistry.put(handler.getType(), handler); handlerRegistry.put(handler.getType(), handler);
} }
} }
public MallInboundResult cancelMainOrder(MallOrderEventDTO dto, boolean paidNotSplit) {
String mainOrderId = StrUtil.blankToDefault(dto.getMainOrderId(), dto.getOrderId());
if (StrUtil.isBlank(mainOrderId)) {
return MallInboundResult.fail(400, "主订单信息不能为空");
}
PointsMallOrderMain main = dao.fetch(PointsMallOrderMain.class, Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "=", mainOrderId).and("delFlag", "=", false));
if (main == null) {
return MallInboundResult.fail(404, "主订单不存在");
}
if (!paidNotSplit && !Integer.valueOf(ORDER_PENDING_PAY).equals(main.getOrderState())) {
return MallInboundResult.fail(400, "只能取消未支付主单");
}
Date now = new Date();
Trans.exec((Atom) () -> {
main.setOrderState(ORDER_CANCELLED);
main.setOrderUpdateTime(now);
dao.updateIgnoreNull(main);
dao.update(PointsMallOrderSub.class, Chain.make("orderState", ORDER_CANCELLED).add("orderUpdateTime", now),
Cnd.where("supplierId", "=", dto.getSupplierId()).and("mainOrderId", "=", mainOrderId).and("delFlag", "=", false));
pointsMallPointsLockService.releaseMain(main.getUserId(), main.getSupplierId(), main.getOrderId());
});
log.info("积分商城主单取消完成,supplierId={}mainOrderId={}paidNotSplit={}", dto.getSupplierId(), mainOrderId, paidNotSplit);
return MallInboundResult.success();
}
public MallInboundResult saveOrderSub(MallOrderEventDTO dto) {
PointsMallOrderMain main = dao.fetch(PointsMallOrderMain.class, Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "=", dto.getMainOrderId()).and("delFlag", "=", false));
if (main == null) {
return MallInboundResult.fail(404, "主订单不存在");
}
List<String> subOrderIds = splitIds(dto.getOrderId());
if (mallOrderQueryOutboundService != null) {
try {
return splitSubOrderByQuery(dto, main, subOrderIds);
} catch (Exception e) {
log.error("split sub order by query failed, fallback to local split logic, supplierId={}, mainOrderId={}, orderId={}",
dto.getSupplierId(), dto.getMainOrderId(), dto.getOrderId(), e);
}
}
if (subOrderIds.isEmpty()) {
return MallInboundResult.fail(400, "子订单信息不能为空");
}
List<OrderProInfoDTO> products = StrUtil.isBlank(main.getExtJson()) || "null".equalsIgnoreCase(main.getExtJson())
? Collections.emptyList()
: JSONUtil.toList(main.getExtJson(), OrderProInfoDTO.class);
List<PointsMallOrderSub> subLocks = new ArrayList<>();
Trans.exec((Atom) () -> {
for (String subOrderId : subOrderIds) {
PointsMallOrderSub sub = dao.fetch(PointsMallOrderSub.class, Cnd.where("orderId", "=", subOrderId)
.and("supplierId", "=", dto.getSupplierId()).and("delFlag", "=", false));
if (sub == null) {
sub = toSub(main, subOrderId);
sub.setOrderState(ORDER_IN_PROGRESS);
dao.insert(sub);
insertProducts(subOrderId, products);
} else {
sub.setOrderState(ORDER_IN_PROGRESS);
sub.setOrderUpdateTime(new Date());
dao.updateIgnoreNull(sub);
}
subLocks.add(sub);
}
pointsMallPointsLockService.createSubLocks(main, subLocks);
});
log.info("积分商城拆单完成,supplierId={}mainOrderId={}subOrderCount={}", dto.getSupplierId(), dto.getMainOrderId(), subOrderIds.size());
return MallInboundResult.success();
}
private MallInboundResult splitSubOrderByQuery(MallOrderEventDTO dto, PointsMallOrderMain main, List<String> subOrderIds) {
log.info("9.2.1 生成订单信息推送-订单状态为:2(拆单子单)开始执行:{}", dto);
if (subOrderIds.isEmpty()) {
return MallInboundResult.fail(400, "缺少拆单信息");
}
log.info("9.2.1 生成订单信息推送-订单状态为:2(拆单子单)调用外部服务获取子单信息:{}", dto);
List<OrderInfoDTO> orderInfoList;
try {
orderInfoList = getOrderInfoList(dto);
} catch (Exception e) {
log.error("9.2.1 生成订单信息推送-订单状态为:2(拆单子单),调用9.2.2获取订单详情接口异常:{}", e.getMessage(), e);
return MallInboundResult.fail(-1, "调用9.2.2获取订单详情接口异常,异常订单:" + subOrderIds);
}
if (orderInfoList == null || orderInfoList.isEmpty()) {
return MallInboundResult.fail(404, "子单不存在");
}
List<PointsMallOrderSub> subLocks = new ArrayList<>();
Trans.exec((Atom) () -> {
for (OrderInfoDTO info : orderInfoList) {
PointsMallOrderSub sub = dao.fetch(PointsMallOrderSub.class, Cnd.where("orderId", "=", info.getOrderId())
.and("supplierId", "=", dto.getSupplierId()).and("delFlag", "=", false));
if (sub == null) {
sub = toSub(main, info);
sub.setOrderState(ORDER_IN_PROGRESS);
dao.insert(sub);
insertProducts(info.getOrderId(), info.getOrderProInfos() == null ? Collections.emptyList() : info.getOrderProInfos());
} else {
log.info("9.2.1 生成订单信息推送-订单状态为:2(拆单子单)子单已存在:{}", info);
sub.setOrderState(ORDER_IN_PROGRESS);
sub.setOrderUpdateTime(new Date());
dao.updateIgnoreNull(sub);
}
subLocks.add(sub);
}
pointsMallPointsLockService.createSubLocks(main, subLocks);
});
log.info("9.2.1 生成订单信息推送-订单状态为:2(拆单子单)结束,supplierId={}mainOrderId={}subOrderCount={}",
dto.getSupplierId(), dto.getMainOrderId(), orderInfoList.size());
return MallInboundResult.success();
}
private List<OrderInfoDTO> getOrderInfoList(MallOrderEventDTO dto) {
MallOrderQueryDTO queryDTO = new MallOrderQueryDTO();
queryDTO.setSupplierId(dto.getSupplierId());
queryDTO.setOrderId(dto.getOrderId());
queryDTO.setOrderType(dto.getOrderType());
MallOutboundResult result = mallOrderQueryOutboundService.orderQuery(queryDTO);
if (result == null || result.getResultCode() != 0) {
throw new RuntimeException(result == null ? "获取订单详情返回为空" : result.getMessage());
}
return result.getOrderInfo();
}
private PointsMallOrderSub toSub(PointsMallOrderMain main, OrderInfoDTO info) {
PointsMallOrderSub sub = toSub(main, info.getOrderId());
sub.setOrderNumberId(info.getOrderNumberId());
sub.setAplName(StrUtil.blankToDefault(info.getAplName(), main.getAplName()));
sub.setOrderCreateTime(parseDate(info.getCreateTime()));
sub.setOrderUpdateTime(parseDate(info.getUpdateTime()));
sub.setDeliveryTime(parseDate(info.getDeliveryTime()));
sub.setOrderFinishTime(parseDate(info.getOrderFinishTime()));
sub.setOrderCompleteTime(parseDate(info.getCompleteTime()));
sub.setDeliveryStatus(parseInteger(info.getDeliveryStatus()));
sub.setFreightCost(nvl(info.getFreightCost()));
sub.setWPayOrAPayFreightCost(nvl(info.getWPayOrAPayFreightCost()));
sub.setRefund(nvl(info.getRefund()));
sub.setWPayOrAPayRefund(nvl(info.getWPayOrAPayRefund()));
sub.setLogisticsOrderId(info.getLogisticsOrderId());
sub.setCrrgBsnNm(info.getCrrgBsnNm());
sub.setTotalPrice(nvl(info.getTotalPrice()));
sub.setWPayOrAPay(nvl(info.getWPayOrAPay()));
sub.setPointsPrice(nvl(info.getPointsPrice()));
sub.setInvoiceCode(info.getInvoiceCode());
sub.setExtJson(JSONUtil.toJsonStr(info.getOrderProInfos() == null ? Collections.emptyList() : info.getOrderProInfos()));
sub.setReconciliationStatus(0);
sub.setInvoiceStatus(0);
return sub;
}
/**
* 更新子单状态。
*
* <p>订单类型为:6(确认收货)时,供应商侧“确认收货”对应 zhgh/callP 侧“已妥投(4)”,
* 需重新调用 9.2.2 获取订单详情接口,补充确认收货时间、发货时间、商品信息等字段。</p>
*
* @param dto 请求参数
* @param statusEnum zhgh/callP 侧订单状态枚举
* @return 操作结果
*/
public MallInboundResult updateSubOrderStatus(MallOrderEventDTO dto, MallOrderStatusEnum statusEnum) {
Integer state = statusEnum.getCode();
PointsMallOrderSub sub = dao.fetch(PointsMallOrderSub.class, Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "=", dto.getOrderId()).and("delFlag", "=", false));
if (sub == null) {
return MallInboundResult.fail(404, "订单不存在");
}
if (Integer.valueOf(state).equals(sub.getOrderState())) {
log.info("积分商城子单状态已是目标状态,跳过重复处理,supplierId={}orderId={}state={}", dto.getSupplierId(), dto.getOrderId(), state);
return MallInboundResult.success();
}
Trans.exec((Atom) () -> {
if (statusEnum == MallOrderStatusEnum.RECEIVED) {
fillReceivedSubOrder(dto, sub);
}
sub.setOrderState(state);
if (statusEnum != MallOrderStatusEnum.RECEIVED || sub.getOrderUpdateTime() == null) {
sub.setOrderUpdateTime(new Date());
}
if (statusEnum == MallOrderStatusEnum.RECEIVED && sub.getOrderFinishTime() == null) {
sub.setOrderFinishTime(new Date());
}
if (statusEnum == MallOrderStatusEnum.COMPLETED) {
sub.setOrderCompleteTime(new Date());
}
dao.updateIgnoreNull(sub);
if (statusEnum == MallOrderStatusEnum.CANCELLED || statusEnum == MallOrderStatusEnum.REFUNDED || statusEnum == MallOrderStatusEnum.ABOLISH) {
pointsMallPointsLockService.releaseOrReturnPoints(sub, state, nvl(sub.getRefund(), BigDecimal.ZERO));
}
if (statusEnum == MallOrderStatusEnum.COMPLETED) {
pointsMallPointsLockService.deductCompletedSub(sub);
}
});
log.info("积分商城子单状态更新完成,supplierId={}orderId={}state={}", dto.getSupplierId(), dto.getOrderId(), state);
return MallInboundResult.success();
}
/**
* 订单类型为:6(确认收货)时,调用 9.2.2 获取订单详情接口,补充子单妥投相关信息。
*
* <p>供应商侧确认收货只代表电商处已收货,对 zhgh/callP 侧应更新为已妥投(4)
* 等待 callP 用户侧确认收货后才进入已完成(5)。</p>
*/
private void fillReceivedSubOrder(MallOrderEventDTO dto, PointsMallOrderSub sub) {
if (mallOrderQueryOutboundService == null) {
return;
}
MallOrderQueryDTO queryDTO = new MallOrderQueryDTO();
queryDTO.setSupplierId(dto.getSupplierId());
queryDTO.setOrderId(dto.getOrderId());
queryDTO.setOrderType(2);
log.info("订单类型为:6(确认收货)时,调用9.2.2获取订单详情接口:{}", queryDTO);
try {
MallOutboundResult result = mallOrderQueryOutboundService.queryOrderInfo(queryDTO);
if (result == null || result.getResultCode() != 0 || result.getOrderInfo() == null || result.getOrderInfo().isEmpty()) {
log.error("订单类型为:6(确认收货)时,调用9.2.2 获取订单详情失败:{},{}", queryDTO, result == null ? null : result.getMessage());
return;
}
OrderInfoDTO orderInfo = result.getOrderInfo().get(0);
sub.setOrderCreateTime(parseDate(orderInfo.getCreateTime()));
sub.setOrderUpdateTime(parseDate(orderInfo.getUpdateTime()));
sub.setOrderFinishTime(parseDate(orderInfo.getOrderFinishTime()));
sub.setDeliveryTime(parseDate(orderInfo.getDeliveryTime()));
sub.setDeliveryStatus(parseInteger(orderInfo.getDeliveryStatus()));
if (orderInfo.getOrderProInfos() != null) {
sub.setExtJson(JSONUtil.toJsonStr(orderInfo.getOrderProInfos()));
}
} catch (Exception e) {
log.error("订单类型为:6(确认收货)时,调用9.2.2 获取订单详情异常:{},{}", queryDTO, e.getMessage());
}
}
public MallInboundResult updateDeliveryStatus(MallOrderEventDTO dto) {
if (StrUtil.isBlank(dto.getSupplierId()) || StrUtil.isBlank(dto.getOrderId())) {
return MallInboundResult.fail(400, "供应商或订单不能为空");
}
List<String> orderIds = splitIds(dto.getOrderId());
if (orderIds.isEmpty()) {
return MallInboundResult.fail(400, "订单信息不能为空");
}
Date deliveryTime = parseDate(dto.getDeliveryTime());
if (deliveryTime == null) {
deliveryTime = new Date();
}
Cnd subCnd = Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "in", orderIds)
.and("delFlag", "=", false);
if (StrUtil.isNotBlank(dto.getUserId())) {
subCnd.and("userId", "=", dto.getUserId());
}
List<PointsMallOrderSub> subs = dao.query(PointsMallOrderSub.class, subCnd);
if (subs.isEmpty()) {
return MallInboundResult.fail(404, "订单不存在");
}
Date finalDeliveryTime = deliveryTime;
Integer deliveredStatus = 1;
Trans.exec((Atom) () -> {
Chain subChain = Chain.make("deliveryStatus", deliveredStatus)
.add("deliveryTime", finalDeliveryTime);
if (StrUtil.isNotBlank(dto.getLogisticsOrderId())) {
subChain.add("logisticsOrderId", dto.getLogisticsOrderId());
}
if (StrUtil.isNotBlank(dto.getCrrgBsnNm())) {
subChain.add("crrgBsnNm", dto.getCrrgBsnNm());
}
dao.update(PointsMallOrderSub.class, subChain, subCnd);
String mainOrderId = StrUtil.blankToDefault(dto.getMainOrderId(), subs.get(0).getMainOrderId());
if (StrUtil.isNotBlank(mainOrderId)) {
Chain mainChain = Chain.make("deliveryStatus", String.valueOf(deliveredStatus))
.add("deliveryTime", finalDeliveryTime);
if (StrUtil.isNotBlank(dto.getLogisticsOrderId())) {
mainChain.add("logisticsOrderId", dto.getLogisticsOrderId());
}
if (StrUtil.isNotBlank(dto.getCrrgBsnNm())) {
mainChain.add("crrgBsnNm", dto.getCrrgBsnNm());
}
dao.update(PointsMallOrderMain.class, mainChain, Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "=", mainOrderId).and("delFlag", "=", false));
}
});
log.info("供应商已发货事件处理完成,supplierId={}mainOrderId={}orderIds={}deliveryStatus={}",
dto.getSupplierId(), dto.getMainOrderId(), orderIds, deliveredStatus);
return MallInboundResult.success();
}
public MallInboundResult invoiceInfo(MallOrderEventDTO dto) {
String mainOrderId = StrUtil.blankToDefault(dto.getMainOrderId(), dto.getOrderId());
if (StrUtil.isBlank(mainOrderId)) {
return MallInboundResult.fail(400, "主订单信息不能为空");
}
dao.update(PointsMallOrderSub.class, Chain.make("invoiceStatus", 1), Cnd.where("supplierId", "=", dto.getSupplierId())
.and("mainOrderId", "=", mainOrderId).and("delFlag", "=", false));
log.info("积分商城发票通知状态更新完成,supplierId={}mainOrderId={}", dto.getSupplierId(), mainOrderId);
return MallInboundResult.success();
}
} }
@@ -2,6 +2,7 @@ package com.budwk.app.zhgh.pointsmall.mallbridge.service;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallPendingOrderDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallPendingOrderDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult; import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport; import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport;
@@ -32,13 +33,17 @@ public class MallInboundPendingOrderService extends AbstractMallBridgeSupport {
if (dto.getOrderInfo() == null || StrUtil.isBlank(dto.getOrderInfo().getOrderId())) { if (dto.getOrderInfo() == null || StrUtil.isBlank(dto.getOrderInfo().getOrderId())) {
return MallInboundResult.fail(400, "订单信息不能为空"); return MallInboundResult.fail(400, "订单信息不能为空");
} }
Sys_user user = fetchUser(dto.getUserId());
if (user == null) {
return MallInboundResult.fail(400, "用户虚拟ID未配置或不存在");
}
log.info("积分商城接收待支付订单开始,supplierId={}orderId={}", dto.getSupplierId(), dto.getOrderInfo().getOrderId()); log.info("积分商城接收待支付订单开始,supplierId={}orderId={}", dto.getSupplierId(), dto.getOrderInfo().getOrderId());
if (dao.count(PointsMallOrderMain.class, Cnd.where("orderId", "=", dto.getOrderInfo().getOrderId()) if (dao.count(PointsMallOrderMain.class, Cnd.where("orderId", "=", dto.getOrderInfo().getOrderId())
.and("supplierId", "=", dto.getSupplierId()).and("delFlag", "=", false)) > 0) { .and("supplierId", "=", dto.getSupplierId()).and("delFlag", "=", false)) > 0) {
return MallInboundResult.fail(-1, "订单已存在"); return MallInboundResult.fail(-1, "订单已存在");
} }
Trans.exec((Atom) () -> { Trans.exec((Atom) () -> {
PointsMallOrderMain main = toMain(dto.getSupplierId(), dto.getUserId(), dto.getOrderInfo(), ORDER_PENDING_PAY); PointsMallOrderMain main = toMain(dto.getSupplierId(), user.getId(), dto.getOrderInfo(), ORDER_PENDING_PAY);
dao.insert(main); dao.insert(main);
// 新增主单历史记录 // 新增主单历史记录
@@ -1,9 +1,13 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.service; package com.budwk.app.zhgh.pointsmall.mallbridge.service;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallPointQueryDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallPointQueryDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundPointsResult; import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundPointsResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport;
import com.budwk.app.zhgh.pointsmall.points.service.PointsMallUserPointsService; import com.budwk.app.zhgh.pointsmall.points.service.PointsMallUserPointsService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@@ -11,17 +15,28 @@ import org.nutz.ioc.loader.annotation.IocBean;
* 商城桥接积分查询服务。 * 商城桥接积分查询服务。
*/ */
@Slf4j @Slf4j
@IocBean @IocBean(args = {"refer:dao"})
public class MallInboundPointsQueryService { public class MallInboundPointsQueryService extends AbstractMallBridgeSupport {
@Inject @Inject
private PointsMallUserPointsService pointsMallUserPointsService; private PointsMallUserPointsService pointsMallUserPointsService;
public MallInboundPointsQueryService(Dao dao) {
super(dao);
}
/** /**
* 查询用户可用积分。 * 查询用户可用积分。
*/ */
public MallInboundPointsResult queryPoints(MallPointQueryDTO dto) { public MallInboundPointsResult queryPoints(MallPointQueryDTO dto) {
log.info("积分商城查询用户积分,supplierId={}userId={}", dto == null ? null : dto.getSupplierId(), dto == null ? null : dto.getUserId()); log.info("积分商城查询用户积分,supplierId={}userId={}", dto == null ? null : dto.getSupplierId(), dto == null ? null : dto.getUserId());
return MallInboundPointsResult.success(pointsMallUserPointsService.availablePoints(dto.getUserId())); if (dto == null || StrUtil.isBlank(dto.getUserId())) {
return MallInboundPointsResult.fail(400, "用户虚拟ID不能为空");
}
Sys_user user = fetchUser(dto.getUserId());
if (user == null) {
return MallInboundPointsResult.fail(400, "用户虚拟ID未配置或不存在");
}
return MallInboundPointsResult.success(pointsMallUserPointsService.availablePoints(user.getId()));
} }
} }
@@ -1,4 +1,5 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.service; package com.budwk.app.zhgh.pointsmall.mallbridge.service;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderMessageDTO;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallPurchaseResultDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallPurchaseResultDTO;
@@ -58,7 +59,7 @@ public class MallInboundPurchaseResultService {
} }
try { try {
// callP消息推送 // callP消息推送
mallCallPMessagePushService.pushMessageToGlobal(orderMain); mallCallPMessagePushService.pushMessageToGlobal("11111111", BeanUtil.copyProperties(orderMain, MallOrderMessageDTO.class));
} catch (Exception e) { } catch (Exception e) {
log.error("9.2.14 支付结果通知接口,callP消息推送:{},{}", dto, e.getMessage()); log.error("9.2.14 支付结果通知接口,callP消息推送:{},{}", dto, e.getMessage());
} }
@@ -1,24 +1,22 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.service; package com.budwk.app.zhgh.pointsmall.mallbridge.service;
import cn.hutool.core.convert.Convert; import cn.hutool.http.HttpUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceInfo; import cn.hutool.json.JSONUtil;
import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceMain; import com.budwk.app.zhgh.pointsmall.mallbridge.util.MallBridgeResponseUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallInvoiceApplyDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallInvoiceApplyDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallOutboundResult; import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallOutboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport; import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub; import com.budwk.app.zhgh.pointsmall.mallbridge.util.MallBridgeCryptoUtil;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.trans.Atom;
import org.nutz.trans.Trans;
import java.math.BigDecimal; import java.util.HashMap;
import java.util.List;
import java.util.Objects;
/** /**
* 商城桥接开票申请出站服务。 * 商城桥接开票申请出站服务。
@@ -27,73 +25,61 @@ import java.util.Objects;
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class MallInvoiceApplyOutboundService extends AbstractMallBridgeSupport { public class MallInvoiceApplyOutboundService extends AbstractMallBridgeSupport {
@Inject
private PropertiesProxy conf;
public MallInvoiceApplyOutboundService(Dao dao) { public MallInvoiceApplyOutboundService(Dao dao) {
super(dao); super(dao);
} }
/** /**
* 接收开票申请并保存开票记录 * 向供应商发起开票申请
*/ */
public MallOutboundResult applyInvoice(MallInvoiceApplyDTO dto) { public MallOutboundResult applyInvoice(MallInvoiceApplyDTO dto) {
if (dto == null || StrUtil.hasBlank(dto.getSupplierId(), dto.getSettlementId(), dto.getOrderIds())) { if (dto == null || StrUtil.hasBlank(dto.getSupplierId(), dto.getSettlementId(), dto.getOrderIds())) {
return MallOutboundResult.fail(400, "开票申请参数不能为空"); return MallOutboundResult.fail(400, "开票申请参数不能为空");
} }
List<String> orderIds = splitIds(dto.getOrderIds()); String clientBAB7ServerUrl = conf.get("spdb.bab7.url", "");
log.info("积分商城开票申请开始,supplierId={}settlementId={}orderCount={}", dto.getSupplierId(), dto.getSettlementId(), orderIds.size()); if (StrUtil.isBlank(clientBAB7ServerUrl)) {
List<PointsMallOrderSub> orders = dao.query(PointsMallOrderSub.class, Cnd.where("supplierId", "=", dto.getSupplierId()) return MallOutboundResult.fail(500, "未配置 BAB7 服务地址");
.and("orderId", "in", orderIds).and("delFlag", "=", false));
if (orders.isEmpty()) {
return MallOutboundResult.fail(404, "未找到可开票订单");
} }
if (dto.getOrderNum() != null && dto.getOrderNum() != orders.size()) { PointsMallSupplier supplier = dao.fetch(PointsMallSupplier.class,
return MallOutboundResult.fail(400, "订单数量与开票申请不一致"); Cnd.where("supplierId", "=", dto.getSupplierId()).and("delFlag", "=", false));
if (supplier == null) {
return MallOutboundResult.fail(404, "未找到对应供应商信息");
} }
BigDecimal total = orders.stream().map(PointsMallOrderSub::getTotalPrice).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); if (StrUtil.isBlank(supplier.getSupplierApiUrl())) {
if (dto.getOrderTotalPrice() != null && total.compareTo(dto.getOrderTotalPrice()) != 0) { return MallOutboundResult.fail(404, "供应商地址未配置");
return MallOutboundResult.fail(400, "订单金额与开票申请不一致");
} }
Trans.exec((Atom) () -> saveInvoiceApply(dto, orderIds, total)); long timestamp = System.currentTimeMillis();
log.info("积分商城开票申请完成,supplierId={}settlementId={}", dto.getSupplierId(), dto.getSettlementId()); dto.setToken(MallBridgeCryptoUtil.generateSign(supplier.getSupplierId(), supplier.getClientId(), timestamp));
return MallOutboundResult.success(); dto.setTimestamp(timestamp);
String keyHex = conf.get("sm4cbc.keyHex", "");
String ivHex = conf.get("sm4cbc.ivHex", "");
String encrypt;
try {
encrypt = MallBridgeCryptoUtil.sm4CbcEncrypt(JSONUtil.toJsonStr(dto), keyHex, ivHex);
} catch (Exception e) {
log.error("9.2.5 发票开具申请接口请求参数 SM4CBC 加密出错", e);
return MallOutboundResult.fail(500, "发票开具申请参数加密失败");
} }
private void saveInvoiceApply(MallInvoiceApplyDTO dto, List<String> orderIds, BigDecimal total) { HashMap<String, Object> bodyMap = new HashMap<>();
PointsMallInvoiceMain main = dao.fetch(PointsMallInvoiceMain.class, Cnd.where("settlementId", "=", dto.getSettlementId()).and("delFlag", "=", false)); bodyMap.put("Address4", supplier.getSupplierApiUrl() + "/supplierOpenApi/pointOrder/applyForInvoices");
if (main == null) { bodyMap.put("BussDealMd", "P");
main = new PointsMallInvoiceMain(); bodyMap.put("SroNo", supplier.getSroNo());
main.setSettlementId(dto.getSettlementId()); bodyMap.put("RsrvFld1", supplier.getClientId());
main.setBSuccess(0); bodyMap.put("AplParmObjct", encrypt);
main.setSucOrderIds(String.join(",", orderIds));
main.setFailOrderIds(""); String response = HttpUtil.createPost(clientBAB7ServerUrl)
main.setFailMsg(""); .contentType("application/json;charset=UTF-8")
dao.insert(main); .body(JSONUtil.toJsonStr(bodyMap))
} else { .execute()
main.setBSuccess(0); .body();
main.setSucOrderIds(String.join(",", orderIds)); if (StrUtil.isBlank(response)) {
main.setFailOrderIds(""); return MallOutboundResult.fail(502, "供应商开票申请无响应");
main.setFailMsg("");
dao.updateIgnoreNull(main);
} }
return JSONUtil.toBean(MallBridgeResponseUtil.parse(response), MallOutboundResult.class);
PointsMallInvoiceInfo info = new PointsMallInvoiceInfo();
info.setMainId(main.getId());
info.setInvoiceId(dto.getSettlementId());
info.setInvoiceCode(dto.getInvoiceCode());
info.setInvoiceDate(parseDate(dto.getInvoiceDate()));
info.setInvoiceNakeAmount(nvl(dto.getInvoiceNakedPrice(), total));
info.setInvoiceTaxAmount(nvl(dto.getInvoiceTaxPrice(), BigDecimal.ZERO));
info.setInvoiceAmount(total);
info.setInvoiceType(Convert.toInt(dto.getInvoiceType(), null));
info.setInvoiceAddress(dto.getInvoiceAddress());
info.setInvoiceContact(dto.getInvoiceContact());
info.setInvoiceContent(dto.getInvoiceContent());
info.setInvoiceBank(dto.getBankName());
info.setInvoiceAccount(dto.getBankAccount());
info.setTaxIdNumber(dto.getInvoiceCode());
info.setRemark(dto.getRemark());
dao.insert(info);
dao.update(PointsMallOrderSub.class, Chain.make("invoiceStatus", 2).add("invoiceCode", dto.getSettlementId()),
Cnd.where("supplierId", "=", dto.getSupplierId()).and("orderId", "in", orderIds));
} }
} }
@@ -1,19 +1,26 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.service; package com.budwk.app.zhgh.pointsmall.mallbridge.service;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceInfo; import cn.hutool.http.HttpUtil;
import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceMain; import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.util.MallBridgeResponseUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.GetInvoiceInfoDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.GetInvoiceInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.InvoiceInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.QueryInvoiceInfoDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.QueryInvoiceInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport; import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport;
import com.budwk.app.zhgh.pointsmall.mallbridge.util.MallBridgeCryptoUtil;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.PictureQueryDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallPictureOutboundResult;
/** /**
* 商城桥接发票信息查询服务。 * 商城桥接发票信息查询服务。
@@ -22,10 +29,51 @@ import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class MallInvoiceInfoQueryService extends AbstractMallBridgeSupport { public class MallInvoiceInfoQueryService extends AbstractMallBridgeSupport {
@Inject
private PropertiesProxy conf;
public MallInvoiceInfoQueryService(Dao dao) { public MallInvoiceInfoQueryService(Dao dao) {
super(dao); super(dao);
} }
public MallPictureOutboundResult queryInvoiceFiles(String supplierId, String settlementId, List<String> fileIds) {
return queryFiles(supplierId, settlementId, "2", fileIds);
}
public MallPictureOutboundResult queryFiles(String supplierId, String idNumber, String queryType, List<String> fileIds) {
PointsMallSupplier supplier = dao.fetch(PointsMallSupplier.class,
Cnd.where("supplierId", "=", supplierId).and("delFlag", "=", false));
if (supplier == null || StrUtil.isBlank(supplier.getSupplierApiUrl())) {
throw new IllegalArgumentException("供应商或供应商接口地址未配置");
}
String apiUrl = conf.get("spdb.bab7.url", "");
if (StrUtil.isBlank(apiUrl)) {
throw new IllegalArgumentException("未配置 BAB7 服务地址");
}
PictureQueryDTO dto = new PictureQueryDTO();
long timestamp = System.currentTimeMillis();
dto.setSupplierId(supplierId);
dto.setToken(MallBridgeCryptoUtil.generateSign(supplierId, supplier.getClientId(), timestamp));
dto.setTimestamp(timestamp);
dto.setQueryType(queryType);
dto.setIdNumber(idNumber);
dto.setFileIdList(fileIds);
HashMap<String, Object> body = new HashMap<>();
body.put("Address4", supplier.getSupplierApiUrl() + "/supplierOpenApi/pointOrder/queryFileBase64Info");
body.put("BussDealMd", "P");
body.put("SroNo", supplier.getSroNo());
body.put("RsrvFld1", supplier.getClientId());
body.put("AplParmObjct", MallBridgeCryptoUtil.sm4CbcEncrypt(JSONUtil.toJsonStr(dto),
conf.get("sm4cbc.keyHex", ""), conf.get("sm4cbc.ivHex", "")));
String response = HttpUtil.createPost(apiUrl).contentType("application/json;charset=UTF-8")
.body(JSONUtil.toJsonStr(body)).execute().body();
JSONObject payload = MallBridgeResponseUtil.parse(response);
if (payload.getInt("resultCode", -1) != 0) {
throw new IllegalArgumentException(StrUtil.blankToDefault(payload.getStr("message"), "获取供应商文件失败"));
}
return JSONUtil.toBean(payload, MallPictureOutboundResult.class);
}
/** /**
* 查询指定结算单的发票信息。 * 查询指定结算单的发票信息。
*/ */
@@ -33,21 +81,57 @@ public class MallInvoiceInfoQueryService extends AbstractMallBridgeSupport {
if (dto == null || StrUtil.hasBlank(dto.getSupplierId(), dto.getSettlementId())) { if (dto == null || StrUtil.hasBlank(dto.getSupplierId(), dto.getSettlementId())) {
throw new IllegalArgumentException("发票查询参数不能为空"); throw new IllegalArgumentException("发票查询参数不能为空");
} }
log.info("积分商城查询发票信息,supplierId={}settlementId={}", dto.getSupplierId(), dto.getSettlementId()); String clientBAB7ServerUrl = conf.get("spdb.bab7.url", "");
PointsMallInvoiceMain main = dao.fetch(PointsMallInvoiceMain.class, Cnd.where("settlementId", "=", dto.getSettlementId()) if (StrUtil.isBlank(clientBAB7ServerUrl)) {
.and("delFlag", "=", false)); throw new IllegalArgumentException("未配置 BAB7 服务地址");
if (main == null) {
throw new IllegalArgumentException("发票信息不存在");
} }
List<InvoiceInfoDTO> invoiceInfos = dao.query(PointsMallInvoiceInfo.class, Cnd.where("mainId", "=", main.getId()).and("delFlag", "=", false)) PointsMallSupplier supplier = dao.fetch(PointsMallSupplier.class,
.stream().map(this::toInvoiceInfoDTO).collect(Collectors.toList()); Cnd.where("supplierId", "=", dto.getSupplierId()).and("delFlag", "=", false));
GetInvoiceInfoDTO result = new GetInvoiceInfoDTO(); if (supplier == null) {
result.setBSuccess(main.getBSuccess()); throw new IllegalArgumentException("未找到对应供应商信息");
result.setSettlementId(main.getSettlementId()); }
result.setSucOrderIds(main.getSucOrderIds()); if (StrUtil.isBlank(supplier.getSupplierApiUrl())) {
result.setFailOrderIds(main.getFailOrderIds()); throw new IllegalArgumentException("供应商地址未配置");
result.setFailMsg(main.getFailMsg()); }
result.setInvoiceInfos(invoiceInfos);
return result; long timestamp = System.currentTimeMillis();
dto.setToken(MallBridgeCryptoUtil.generateSign(supplier.getSupplierId(), supplier.getClientId(), timestamp));
dto.setTimestamp(timestamp);
String encrypt;
try {
encrypt = MallBridgeCryptoUtil.sm4CbcEncrypt(
JSONUtil.toJsonStr(dto),
conf.get("sm4cbc.keyHex", ""),
conf.get("sm4cbc.ivHex", "")
);
} catch (Exception e) {
log.error("9.2.10 发票获取接口请求参数 SM4CBC 加密出错", e);
throw new IllegalArgumentException("发票获取请求参数加密失败");
}
HashMap<String, Object> bodyMap = new HashMap<>();
bodyMap.put("Address4", supplier.getSupplierApiUrl() + "/supplierOpenApi/pointOrder/queryInvoicesInfo");
bodyMap.put("BussDealMd", "P");
bodyMap.put("SroNo", supplier.getSroNo());
bodyMap.put("RsrvFld1", supplier.getClientId());
bodyMap.put("AplParmObjct", encrypt);
String response = HttpUtil.createPost(clientBAB7ServerUrl)
.contentType("application/json;charset=UTF-8")
.body(JSONUtil.toJsonStr(bodyMap))
.execute()
.body();
if (StrUtil.isBlank(response)) {
throw new IllegalArgumentException("没有查询到发票信息");
}
JSONObject responseJson = MallBridgeResponseUtil.parse(response);
if (responseJson.getInt("resultCode", -1) != 0) {
throw new IllegalArgumentException(StrUtil.blankToDefault(responseJson.getStr("message"), "获取发票信息失败"));
}
JSONObject invoiceInfo = responseJson.getJSONObject("getInvoiceInfo");
if (invoiceInfo == null) {
throw new IllegalArgumentException("没有查询到发票信息");
}
return JSONUtil.toBean(invoiceInfo, GetInvoiceInfoDTO.class);
} }
} }
@@ -2,6 +2,7 @@ package com.budwk.app.zhgh.pointsmall.mallbridge.service;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderQueryDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderQueryDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallOutboundResult; import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallOutboundResult;
@@ -85,6 +86,31 @@ public class MallOrderQueryOutboundService {
.execute() .execute()
.body(); .body();
log.info("9.2.2 获取订单详情调用结束,返回报文:{}", response); log.info("9.2.2 获取订单详情调用结束,返回报文:{}", response);
return JSONUtil.toBean(response, MallOutboundResult.class); JSONObject payload = JSONUtil.parseObj(response);
if (!payload.containsKey("resultCode")) {
JSONObject data = payload.getJSONObject("data");
JSONObject body = data == null ? null : data.getJSONObject("Body");
if (body == null) {
return MallOutboundResult.fail(502, "获取订单详情返回报文格式异常");
}
JSONObject header = body.getJSONObject("RspSvcHeader");
if (header == null || !"000000000000".equals(header.getStr("ReturnCode"))) {
return MallOutboundResult.fail(502, header == null ? "BAB7返回服务头缺失" :
StrUtil.blankToDefault(header.getStr("ReturnMsg"), "BAB7获取订单详情失败"));
}
JSONObject serviceBody = body.getJSONObject("RspSvcBody");
if (serviceBody == null || !"200".equals(serviceBody.getStr("ReturnStCd"))) {
return MallOutboundResult.fail(502, "BAB7调用供应商订单详情接口失败");
}
String content = serviceBody.getStr("ReturnCntnt");
if (StrUtil.isBlank(content)) {
return MallOutboundResult.fail(502, "供应商订单详情返回内容为空");
}
payload = JSONUtil.parseObj(content);
}
if (payload.getInt("resultCode") == null) {
return MallOutboundResult.fail(502, "供应商订单详情返回状态缺失");
}
return JSONUtil.toBean(payload, MallOutboundResult.class);
} }
} }
@@ -0,0 +1,185 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.service;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.OrderInfoByReconciliationDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.QueryReconciliationInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallOutboundPageResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.util.MallBridgeCryptoUtil;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
/**
* 获取对账信息
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class MallReconciliationInfoQueryService {
private final Dao dao;
@Inject
private PropertiesProxy conf;
public MallReconciliationInfoQueryService(Dao dao) {
this.dao = dao;
}
public List<OrderInfoByReconciliationDTO> queryReconciliationInfo(QueryReconciliationInfoDTO dto) {
// 从供应商获取对账订单信息
return getOrderInfoFromSupplier(dto);
}
private List<OrderInfoByReconciliationDTO> getOrderInfoFromSupplier(QueryReconciliationInfoDTO dto) {
log.info("9.2.4 对账接口执行开始:{}", dto);
String clientBAB7ServerUrl = conf.get("spdb.bab7.url", "");
if (StrUtil.isBlank(clientBAB7ServerUrl)) {
throw new IllegalArgumentException("未配置 BAB7 服务地址");
}
PointsMallSupplier supplier = dao.fetch(PointsMallSupplier.class,
Cnd.where("supplierId", "=", dto.getSupplierId()).and("delFlag", "=", false));
if (supplier == null) {
log.error("9.2.4 对账接口 未找到对应供应商信息");
throw new IllegalArgumentException("未找到对应供应商信息");
}
long timestamp = System.currentTimeMillis();
// 计算token
String token = MallBridgeCryptoUtil.generateSign(supplier.getSupplierId(), supplier.getClientId(), timestamp);
dto.setToken(token);
dto.setTimestamp(timestamp);
// 组装请求报文
HashMap<String, Object> bodyMap = buildReqParam(dto, supplier);
log.info("9.2.4 通过对接接口从供应商获取对账订单数据:{}", dto);
String response = HttpUtil.createPost(clientBAB7ServerUrl)
.contentType("application/json;charset=UTF-8")
.body(JSONUtil.toJsonStr(bodyMap))
.execute()
.body();
log.info("通过对账接口获取对账订单数据,响应结果:{}", response);
MallOutboundPageResult mallOutboundPageResult = parseReconciliationResponse(response);
if (CollUtil.isEmpty(mallOutboundPageResult.getOrderInfos())) {
return Collections.emptyList();
}
List<OrderInfoByReconciliationDTO> orderInfoList = new ArrayList<>(mallOutboundPageResult.getOrderInfos());
// 页码
Integer page = dto.getPage();
// 总页数
Integer totalPage = mallOutboundPageResult.getTotalPage();
// 当返回的总页数没有变化,以传入的page来循环拉取
while (page < totalPage) {
// 循环拉取page页的数据
dto.setPage(page + 1);
log.info("9.2.4 对账接口-分页循环拉取,入参:{}", dto);
bodyMap = buildReqParam(dto, supplier);
response = HttpUtil.createPost(clientBAB7ServerUrl)
.contentType("application/json;charset=UTF-8")
.body(JSONUtil.toJsonStr(bodyMap))
.execute()
.body();
mallOutboundPageResult = parseReconciliationResponse(response);
log.info("9.2.4 对账接口-分页循环拉取,返回报文:{}", mallOutboundPageResult);
if (CollUtil.isEmpty(mallOutboundPageResult.getOrderInfos())) {
throw new IllegalArgumentException("供应商对账分页数据为空,页码:" + dto.getPage());
}
orderInfoList.addAll(mallOutboundPageResult.getOrderInfos());
page++;
}
log.info("9.2.4 通过对接接口从供应商获取对账订单数据调用结束,返回报文:{}", orderInfoList);
return orderInfoList;
}
/**
* 解析 BAB7 包装或供应商直接返回的对账报文,并校验查询结果。
*/
private MallOutboundPageResult parseReconciliationResponse(String response) {
if (StrUtil.isBlank(response)) {
throw new IllegalArgumentException("获取对账信息返回报文为空");
}
JSONObject payload = JSONUtil.parseObj(response);
if (!payload.containsKey("resultCode")) {
JSONObject data = payload.getJSONObject("data");
JSONObject body = data == null ? null : data.getJSONObject("Body");
if (body == null) {
throw new IllegalArgumentException("获取对账信息返回报文格式异常");
}
JSONObject header = body.getJSONObject("RspSvcHeader");
if (header == null || !"000000000000".equals(header.getStr("ReturnCode"))) {
throw new IllegalArgumentException(header == null ? "BAB7返回服务头缺失" :
StrUtil.blankToDefault(header.getStr("ReturnMsg"), "BAB7获取对账信息失败"));
}
JSONObject serviceBody = body.getJSONObject("RspSvcBody");
if (serviceBody == null || !"200".equals(serviceBody.getStr("ReturnStCd"))) {
throw new IllegalArgumentException("BAB7调用供应商对账接口失败");
}
String content = serviceBody.getStr("ReturnCntnt");
if (StrUtil.isBlank(content)) {
throw new IllegalArgumentException("供应商对账接口返回内容为空");
}
payload = JSONUtil.parseObj(content);
}
Integer resultCode = payload.getInt("resultCode");
if (resultCode == null) {
throw new IllegalArgumentException("供应商对账接口返回状态缺失");
}
if (resultCode != 0) {
throw new IllegalArgumentException(StrUtil.blankToDefault(payload.getStr("message"), "供应商对账查询失败"));
}
MallOutboundPageResult result = JSONUtil.toBean(payload, MallOutboundPageResult.class);
if (CollUtil.isNotEmpty(result.getOrderInfos()) && (result.getTotalPage() == null || result.getTotalPage() < 1)) {
throw new IllegalArgumentException("供应商对账接口返回总页数无效");
}
return result;
}
/**
* 组装请求报文
*
* @param dto 请求参数
* @param supplier 供应商信息
* @return 对账接口请求入参
*/
private HashMap<String, Object> buildReqParam(QueryReconciliationInfoDTO dto, PointsMallSupplier supplier) {
// 每个供应商的接口地址不一致,后续判断
String address4 = supplier.getSupplierApiUrl() + "/supplierOpenApi/pointOrder/queryReconciliationInfo";
String bussDealMd = "P";
String srvNo = supplier.getSroNo();
String clientId = supplier.getClientId();
// 参数(此处需要加密)
String params = JSONUtil.toJsonStr(dto);
log.info("9.2.4 对账接口-请求参数:{}", params);
String keyHex = conf.get("sm4cbc.keyHex", "");
String ivHex = conf.get("sm4cbc.ivHex", "");
String encrypt;
try {
encrypt = MallBridgeCryptoUtil.sm4CbcEncrypt(params, keyHex, ivHex);
} catch (Exception e) {
log.error("9.2.4 对账接口请求参数 SM4CBC 加密出错:{}", params, e);
throw new RuntimeException("9.2.4 对账接口请求参数 SM4CBC 加密出错");
}
HashMap<String, Object> bodyMap = new HashMap<>();
bodyMap.put("Address4", address4);
bodyMap.put("BussDealMd", bussDealMd);
bodyMap.put("SroNo", srvNo);
bodyMap.put("RsrvFld1", clientId);
bodyMap.put("AplParmObjct", encrypt);
return bodyMap;
}
}
@@ -0,0 +1,70 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.service;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.ReconciliationResultNoticeDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.util.MallBridgeCryptoUtil;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.HashMap;
/**
* 对账结果通知服务
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class MallReconciliationResultNoticeService {
private final Dao dao;
@Inject
private PropertiesProxy conf;
public MallReconciliationResultNoticeService(Dao dao) {
this.dao = dao;
}
public MallInboundResult reconciliationResultNotice(ReconciliationResultNoticeDTO dto) {
String clientBAB7ServerUrl = conf.get("spdb.bab7.url", "");
String keyHex = conf.get("sm4cbc.keyHex", "");
String ivHex = conf.get("sm4cbc.ivHex", "");
if (StrUtil.isBlank(clientBAB7ServerUrl)) {
throw new IllegalArgumentException("未配置BAB7服务地址");
}
PointsMallSupplier supplier = dao.fetch(PointsMallSupplier.class,
Cnd.where("supplierId", "=", dto.getSupplierId()).and("delFlag", "=", false));
if (supplier == null || StrUtil.isBlank(supplier.getSupplierApiUrl())) {
throw new IllegalArgumentException("未找到供应商接口地址");
}
long timestamp = System.currentTimeMillis();
dto.setTimestamp(timestamp);
dto.setToken(MallBridgeCryptoUtil.generateSign(supplier.getSupplierId(), supplier.getClientId(), timestamp));
String params = JSONUtil.toJsonStr(dto);
String encrypt = MallBridgeCryptoUtil.sm4CbcEncrypt(params, keyHex, ivHex);
HashMap<String, Object> bodyMap = new HashMap<>();
bodyMap.put("Address4", supplier.getSupplierApiUrl() + "/supplierOpenApi/pointOrder/reconciliationResult");
bodyMap.put("BussDealMd", "P");
bodyMap.put("SroNo", supplier.getSroNo());
bodyMap.put("RsrvFld1", supplier.getClientId());
bodyMap.put("AplParmObjct", encrypt);
String response = HttpUtil.createPost(clientBAB7ServerUrl)
.contentType("application/json;charset=UTF-8")
.body(JSONUtil.toJsonStr(bodyMap))
.execute()
.body();
log.info("对账结果通知接口返回报文:{}", response);
return JSONUtil.toBean(response, MallInboundResult.class);
}
}
@@ -2,25 +2,36 @@ package com.budwk.app.zhgh.pointsmall.mallbridge.service;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.UnReconciledMsgNoticeDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.UnReconciledMsgNoticeDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult; import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport; import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport;
import com.budwk.app.zhgh.pointsmall.mallbridge.util.MallBridgeCryptoUtil;
import com.budwk.app.zhgh.pointsmall.reconciliation.models.PointsMallReconciliationRecord; import com.budwk.app.zhgh.pointsmall.reconciliation.models.PointsMallReconciliationRecord;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.trans.Atom; import org.nutz.trans.Atom;
import org.nutz.trans.Trans; import org.nutz.trans.Trans;
import java.util.HashMap;
import java.util.List; import java.util.List;
/** /**
* 商城桥接对账异常通知服务 * 对账异常消息通知服务
*/ */
@Slf4j @Slf4j
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class MallUnReconciledMsgNoticeService extends AbstractMallBridgeSupport { public class MallUnReconciledMsgNoticeService extends AbstractMallBridgeSupport {
@Inject
private PropertiesProxy conf;
public MallUnReconciledMsgNoticeService(Dao dao) { public MallUnReconciledMsgNoticeService(Dao dao) {
super(dao); super(dao);
} }
@@ -50,4 +61,61 @@ public class MallUnReconciledMsgNoticeService extends AbstractMallBridgeSupport
log.info("积分商城对账异常通知完成,supplierId={}orderCount={}", dto.getSupplierId(), orderIds.size()); log.info("积分商城对账异常通知完成,supplierId={}orderCount={}", dto.getSupplierId(), orderIds.size());
return MallInboundResult.success(); return MallInboundResult.success();
} }
public MallInboundResult unReconciledMsgNotice(UnReconciledMsgNoticeDTO dto) {
log.info("9.3.1 对账异常通知接口调用开始:{}", dto);
String clientBAB7ServerUrl = conf.get("spdb.bab7.url", "");
String keyHex = conf.get("sm4cbc.keyHex", "");
String ivHex = conf.get("sm4cbc.ivHex", "");
if (StrUtil.isBlank(clientBAB7ServerUrl)) {
throw new IllegalArgumentException("未配置BAB7服务地址");
}
// 查询供应商信息
PointsMallSupplier supplier = dao.fetch(PointsMallSupplier.class,
Cnd.where("supplierId", "=", dto.getSupplierId()).and("delFlag", "=", false));
if (supplier == null) {
log.error("9.3.1 对账异常通知接口 未找到对应供应商信息");
throw new IllegalArgumentException("未找到对应供应商信息");
}
if (StrUtil.isBlank(supplier.getSupplierApiUrl())) {
return MallInboundResult.fail(404, "供应商地址未配置");
}
// 组装请求地址
String address4 = supplier.getSupplierApiUrl() + "/supplierOpenApi/pointOrder/unReconciledMsg";
String bussDealMd = "P";
String sroNo = supplier.getSroNo();
String clientId = supplier.getClientId();
long timestamp = System.currentTimeMillis();
dto.setTimestamp(timestamp);
dto.setToken(MallBridgeCryptoUtil.generateSign(supplier.getSupplierId(), clientId, timestamp));
// 参数加密
String params = JSONUtil.toJsonStr(dto);
log.info("9.3.1 对账异常通知接口-请求参数:{}", params);
String encrypt;
try {
encrypt = MallBridgeCryptoUtil.sm4CbcEncrypt(params, keyHex, ivHex);
} catch (Exception e) {
log.error("9.3.1 对账异常通知接口参数加密出错:{}", params, e);
throw new IllegalArgumentException("参数加密失败");
}
// 构建请求体
HashMap<String, Object> bodyMap = new HashMap<>();
bodyMap.put("Address4", address4);
bodyMap.put("BussDealMd", bussDealMd);
bodyMap.put("SroNo", sroNo);
bodyMap.put("RsrvFld1", clientId);
bodyMap.put("AplParmObjct", encrypt);
// 发送请求
String response = HttpUtil.createPost(clientBAB7ServerUrl)
.contentType("application/json;charset=UTF-8")
.body(JSONUtil.toJsonStr(bodyMap))
.execute()
.body();
log.info("9.3.1 对账异常通知接口返回报文:{}", response);
return JSONUtil.toBean(response, MallInboundResult.class);
}
} }
@@ -34,43 +34,105 @@ import java.util.stream.Collectors;
* <p>只放通用转换和常量,具体接口业务放在各自服务中。</p> * <p>只放通用转换和常量,具体接口业务放在各自服务中。</p>
*/ */
public abstract class AbstractMallBridgeSupport { public abstract class AbstractMallBridgeSupport {
/**
* 访问令牌有效期,单位为秒(1 小时)。
*/
protected static final long ACCESS_EXPIRE_SECONDS = 3600; protected static final long ACCESS_EXPIRE_SECONDS = 3600;
/**
* 刷新令牌有效期,单位为秒(7 天)。
*/
protected static final long REFRESH_EXPIRE_SECONDS = 86400L * 7; protected static final long REFRESH_EXPIRE_SECONDS = 86400L * 7;
/**
* 订单状态:待支付。
*/
protected static final int ORDER_PENDING_PAY = 0; protected static final int ORDER_PENDING_PAY = 0;
/**
* 订单状态:已支付。
*/
protected static final int ORDER_FINISH_PAY = 1; protected static final int ORDER_FINISH_PAY = 1;
/**
* 订单状态:处理中。
*/
protected static final int ORDER_IN_PROGRESS = 2; protected static final int ORDER_IN_PROGRESS = 2;
/**
* 订单状态:已取消。
*/
protected static final int ORDER_CANCELLED = 3; protected static final int ORDER_CANCELLED = 3;
/**
* 订单状态:已收货。
*/
protected static final int ORDER_RECEIVED = 4; protected static final int ORDER_RECEIVED = 4;
/**
* 订单状态:已完成。
*/
protected static final int ORDER_COMPLETED = 5; protected static final int ORDER_COMPLETED = 5;
/**
* 订单状态:退换货。
*/
protected static final int ORDER_EXCHANGE = 6; protected static final int ORDER_EXCHANGE = 6;
/**
* 订单状态:已退款。
*/
protected static final int ORDER_REFUNDED = 7; protected static final int ORDER_REFUNDED = 7;
/**
* 订单状态:已作废。
*/
protected static final int ORDER_ABOLISH = 8; protected static final int ORDER_ABOLISH = 8;
/**
* 供桥接服务查询和保存用户、订单、商品等数据的 DAO。
*/
protected final Dao dao; protected final Dao dao;
/**
* 初始化桥接公共支撑类,复用业务服务传入的 DAO。
*/
protected AbstractMallBridgeSupport(Dao dao) { protected AbstractMallBridgeSupport(Dao dao) {
this.dao = dao; this.dao = dao;
} }
/**
* 检查订单事件及供应商、订单、主订单、用户标识是否齐全;不校验对应数据是否存在。
*/
protected boolean hasRequiredOrderEventFields(MallOrderEventDTO dto) { protected boolean hasRequiredOrderEventFields(MallOrderEventDTO dto) {
return dto != null && !StrUtil.hasBlank(dto.getSupplierId(), dto.getOrderId(), dto.getMainOrderId(), dto.getUserId()); return dto != null && !StrUtil.hasBlank(dto.getSupplierId(), dto.getOrderId(), dto.getMainOrderId(), dto.getUserId());
} }
protected Sys_user fetchUser(String userId) { /**
Sys_user user = dao.fetch(Sys_user.class, Cnd.where("id", "=", userId).and("disabled", "=", false).and("delFlag", "=", false)); * 根据对外用户虚拟 ID 查询未禁用、未删除的系统用户。
if (user != null) { *
return user; * @param virtualId 供应商接口传入的用户虚拟 ID,不是系统用户主键
* @return 符合条件的用户;虚拟 ID 为空或用户不存在时返回 null
*/
protected Sys_user fetchUser(String virtualId) {
if (StrUtil.isBlank(virtualId)) {
return null;
} }
return dao.fetch(Sys_user.class, Cnd.where("loginname", "=", userId).and("disabled", "=", false).and("delFlag", "=", false)); return dao.fetch(Sys_user.class, Cnd.where("virtual_id", "=", virtualId)
.and("disabled", "=", false).and("delFlag", "=", false));
} }
/**
* 将退换货事件和详情转换为退换货记录,并从原子单补充商品图片及文件信息。
*
* @param dto 订单事件,其中 userId 为用户虚拟 ID,保存时转换为系统用户主键
* @param orderInfo 供应商返回的退换货单详情
* @param sub 原子单,提供商品扩展信息
* @param orderStatus 待保存的退换货订单状态
* @return 尚未入库的退换货记录
* @throws IllegalArgumentException 虚拟 ID 对应的有效用户不存在时抛出
*/
protected PointsMallOrderExchange toExchange(MallOrderEventDTO dto, OrderInfoDTO orderInfo, PointsMallOrderSub sub, Integer orderStatus) { protected PointsMallOrderExchange toExchange(MallOrderEventDTO dto, OrderInfoDTO orderInfo, PointsMallOrderSub sub, Integer orderStatus) {
Sys_user user = fetchUser(dto.getUserId());
if (user == null) {
throw new IllegalArgumentException("用户虚拟ID未配置或不存在");
}
PointsMallOrderExchange exchange = new PointsMallOrderExchange(); PointsMallOrderExchange exchange = new PointsMallOrderExchange();
exchange.setOrderMainId(dto.getMainOrderId()); exchange.setOrderMainId(dto.getMainOrderId());
exchange.setOrderId(dto.getOrderId()); exchange.setOrderId(dto.getOrderId());
exchange.setSupplierId(dto.getSupplierId()); exchange.setSupplierId(dto.getSupplierId());
exchange.setUserId(dto.getUserId()); exchange.setUserId(user.getId());
exchange.setAplName(orderInfo.getAplName()); exchange.setAplName(orderInfo.getAplName());
exchange.setOrderCreateTime(parseDate(orderInfo.getCreateTime())); exchange.setOrderCreateTime(parseDate(orderInfo.getCreateTime()));
exchange.setOrderUpdateTime(parseDate(orderInfo.getUpdateTime())); exchange.setOrderUpdateTime(parseDate(orderInfo.getUpdateTime()));
@@ -90,6 +152,9 @@ public abstract class AbstractMallBridgeSupport {
return exchange; return exchange;
} }
/**
* 按 SKU 将原子单的图片及文件信息覆盖到传入商品列表中;原子单重复 SKU 取首项,传入列表为空时返回空列表。
*/
protected List<OrderProInfoDTO> mergeProductInfo(List<OrderProInfoDTO> products, String subExtJson) { protected List<OrderProInfoDTO> mergeProductInfo(List<OrderProInfoDTO> products, String subExtJson) {
if (products == null || products.isEmpty()) { if (products == null || products.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
@@ -111,6 +176,15 @@ public abstract class AbstractMallBridgeSupport {
return products; return products;
} }
/**
* 将供应商订单详情转换为主订单,金额为空时按零处理,现金支付标记初始化为 0。
*
* @param supplierId 供应商标识
* @param userId 已由虚拟 ID 查得的系统用户主键
* @param orderInfo 供应商返回的订单详情
* @param state 主订单状态
* @return 尚未入库的主订单
*/
protected PointsMallOrderMain toMain(String supplierId, String userId, OrderInfoDTO orderInfo, Integer state) { protected PointsMallOrderMain toMain(String supplierId, String userId, OrderInfoDTO orderInfo, Integer state) {
PointsMallOrderMain main = new PointsMallOrderMain(); PointsMallOrderMain main = new PointsMallOrderMain();
main.setOrderId(orderInfo.getOrderId()); main.setOrderId(orderInfo.getOrderId());
@@ -138,6 +212,9 @@ public abstract class AbstractMallBridgeSupport {
return main; return main;
} }
/**
* 从主订单复制信息生成子订单;子订单号为空时使用主订单号,更新时间取当前时间,对账和发票状态初始化为 0。
*/
protected PointsMallOrderSub toSub(PointsMallOrderMain main, String subOrderId) { protected PointsMallOrderSub toSub(PointsMallOrderMain main, String subOrderId) {
PointsMallOrderSub sub = new PointsMallOrderSub(); PointsMallOrderSub sub = new PointsMallOrderSub();
sub.setOrderId(StrUtil.blankToDefault(subOrderId, main.getOrderId())); sub.setOrderId(StrUtil.blankToDefault(subOrderId, main.getOrderId()));
@@ -167,6 +244,9 @@ public abstract class AbstractMallBridgeSupport {
return sub; return sub;
} }
/**
* 将商品逐条保存到指定子订单,仅保存图片列表中的首张图片;商品列表为 null 时不处理。
*/
protected void insertProducts(String orderSubId, List<OrderProInfoDTO> products) { protected void insertProducts(String orderSubId, List<OrderProInfoDTO> products) {
if (products == null) { if (products == null) {
return; return;
@@ -184,6 +264,9 @@ public abstract class AbstractMallBridgeSupport {
} }
} }
/**
* 将查询记录转换为对外订单详情,统一日期和金额格式,并读取订单商品信息。
*/
protected OrderInfoDTO toOrderInfo(Record record) { protected OrderInfoDTO toOrderInfo(Record record) {
OrderInfoDTO dto = new OrderInfoDTO(); OrderInfoDTO dto = new OrderInfoDTO();
dto.setOrderId(record.getString("orderId")); dto.setOrderId(record.getString("orderId"));
@@ -211,6 +294,9 @@ public abstract class AbstractMallBridgeSupport {
return dto; return dto;
} }
/**
* 优先读取订单下未删除的子单商品;无商品记录时从 extJson 解析,扩展信息为空或为 null 字符串时返回空列表。
*/
protected List<OrderProInfoDTO> readProducts(String orderId, String extJson) { protected List<OrderProInfoDTO> readProducts(String orderId, String extJson) {
List<PointsMallOrderSubProduct> products = dao.query(PointsMallOrderSubProduct.class, Cnd.where("orderSubId", "=", orderId).and("delFlag", "=", false)); List<PointsMallOrderSubProduct> products = dao.query(PointsMallOrderSubProduct.class, Cnd.where("orderSubId", "=", orderId).and("delFlag", "=", false));
if (!products.isEmpty()) { if (!products.isEmpty()) {
@@ -222,6 +308,9 @@ public abstract class AbstractMallBridgeSupport {
return JSONUtil.toList(extJson, OrderProInfoDTO.class); return JSONUtil.toList(extJson, OrderProInfoDTO.class);
} }
/**
* 将子单商品转换为对外商品信息,单张图片包装为列表,并以 SKU 和图片地址填充文件标识及路径。
*/
protected OrderProInfoDTO toProductDTO(PointsMallOrderSubProduct product) { protected OrderProInfoDTO toProductDTO(PointsMallOrderSubProduct product) {
OrderProInfoDTO dto = new OrderProInfoDTO(); OrderProInfoDTO dto = new OrderProInfoDTO();
dto.setSku(product.getSku()); dto.setSku(product.getSku());
@@ -235,6 +324,9 @@ public abstract class AbstractMallBridgeSupport {
return dto; return dto;
} }
/**
* 将发票记录转换为对外发票信息,并将开票时间转换为本地日期。
*/
protected InvoiceInfoDTO toInvoiceInfoDTO(PointsMallInvoiceInfo info) { protected InvoiceInfoDTO toInvoiceInfoDTO(PointsMallInvoiceInfo info) {
InvoiceInfoDTO dto = new InvoiceInfoDTO(); InvoiceInfoDTO dto = new InvoiceInfoDTO();
dto.setInvoiceId(info.getInvoiceId()); dto.setInvoiceId(info.getInvoiceId());
@@ -253,6 +345,9 @@ public abstract class AbstractMallBridgeSupport {
return dto; return dto;
} }
/**
* 解析日期字符串,将日期时间分隔符 T 替换为空格;空白值返回 null,非法日期由日期工具抛出异常。
*/
protected Date parseDate(String value) { protected Date parseDate(String value) {
if (StrUtil.isBlank(value)) { if (StrUtil.isBlank(value)) {
return null; return null;
@@ -260,6 +355,9 @@ public abstract class AbstractMallBridgeSupport {
return DateUtil.parse(value.replace("T", " ")); return DateUtil.parse(value.replace("T", " "));
} }
/**
* 将日期值统一格式化为 yyyy-MM-dd HH:mm:ssnull 值返回 null。
*/
protected String formatDate(Object value) { protected String formatDate(Object value) {
if (value == null) { if (value == null) {
return null; return null;
@@ -267,10 +365,16 @@ public abstract class AbstractMallBridgeSupport {
return DateUtil.format(Convert.toDate(value), "yyyy-MM-dd HH:mm:ss"); return DateUtil.format(Convert.toDate(value), "yyyy-MM-dd HH:mm:ss");
} }
/**
* 使用系统默认时区将时间转换为日期;null 值返回 null。
*/
protected LocalDate toLocalDate(Date date) { protected LocalDate toLocalDate(Date date) {
return date == null ? null : date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); return date == null ? null : date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
} }
/**
* 拆分逗号分隔的标识,去除首尾空白、空项和重复项,保留首次出现的顺序;空白值返回空列表。
*/
protected List<String> splitIds(String value) { protected List<String> splitIds(String value) {
if (StrUtil.isBlank(value)) { if (StrUtil.isBlank(value)) {
return Collections.emptyList(); return Collections.emptyList();
@@ -282,18 +386,30 @@ public abstract class AbstractMallBridgeSupport {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
/**
* 金额为 null 时返回零,否则保留原值。
*/
protected BigDecimal nvl(BigDecimal value) { protected BigDecimal nvl(BigDecimal value) {
return value == null ? BigDecimal.ZERO : value; return value == null ? BigDecimal.ZERO : value;
} }
/**
* 金额为 null 时返回指定默认值,否则保留原值。
*/
protected BigDecimal nvl(BigDecimal value, BigDecimal defaultValue) { protected BigDecimal nvl(BigDecimal value, BigDecimal defaultValue) {
return value == null ? defaultValue : value; return value == null ? defaultValue : value;
} }
/**
* 将对象转换为金额,无法转换或值为 null 时使用零作为默认值。
*/
protected BigDecimal decimal(Object value) { protected BigDecimal decimal(Object value) {
return Convert.toBigDecimal(value, BigDecimal.ZERO); return Convert.toBigDecimal(value, BigDecimal.ZERO);
} }
/**
* 将字符串转换为整数;空白值或无法转换的值返回 null。
*/
protected Integer parseInteger(String value) { protected Integer parseInteger(String value) {
if (StrUtil.isBlank(value)) { if (StrUtil.isBlank(value)) {
return null; return null;
@@ -0,0 +1,37 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.util;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
/** 提取供应商业务响应,兼容 BAB7 包装报文。 */
public class MallBridgeResponseUtil {
public static JSONObject parse(String response) {
if (StrUtil.isBlank(response)) {
throw new IllegalArgumentException("供应商接口返回为空");
}
JSONObject payload = JSONUtil.parseObj(response);
if (!payload.containsKey("resultCode")) {
JSONObject data = payload.getJSONObject("data");
JSONObject body = data == null ? null : data.getJSONObject("Body");
JSONObject header = body == null ? null : body.getJSONObject("RspSvcHeader");
if (header == null || !"000000000000".equals(header.getStr("ReturnCode"))) {
throw new IllegalArgumentException(header == null ? "BAB7返回报文格式异常" :
StrUtil.blankToDefault(header.getStr("ReturnMsg"), "BAB7调用失败"));
}
JSONObject serviceBody = body.getJSONObject("RspSvcBody");
if (serviceBody == null || !"200".equals(serviceBody.getStr("ReturnStCd"))) {
throw new IllegalArgumentException("BAB7调用供应商接口失败");
}
String content = serviceBody.getStr("ReturnCntnt");
if (StrUtil.isBlank(content)) {
throw new IllegalArgumentException("供应商业务响应为空");
}
payload = JSONUtil.parseObj(content);
}
if (payload.getInt("resultCode") == null) {
throw new IllegalArgumentException("供应商业务响应缺少结果码");
}
return payload;
}
}
@@ -2,33 +2,98 @@ package com.budwk.app.zhgh.pointsmall.order.service.impl;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.enums.SysFileEngineTypeEnum;
import com.budwk.app.sys.services.SysFileService;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.OrderInfoDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.OrderInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.OrderProInfoDTO; import com.budwk.app.zhgh.pointsmall.mallbridge.dto.OrderProInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallPictureOutboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallInvoiceInfoQueryService;
import com.budwk.app.zhgh.pointsmall.order.enums.MallOrderStatusEnum; import com.budwk.app.zhgh.pointsmall.order.enums.MallOrderStatusEnum;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub; import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSubHistory; import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSubHistory;
import com.budwk.app.zhgh.pointsmall.order.result.SubOrderBuildResult; import com.budwk.app.zhgh.pointsmall.order.result.SubOrderBuildResult;
import com.budwk.app.zhgh.pointsmall.order.service.PointsMallOrderSubHistoryService; import com.budwk.app.zhgh.pointsmall.order.service.PointsMallOrderSubHistoryService;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.io.ByteArrayInputStream;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class PointsMallOrderSubHistoryServiceImpl extends BaseServiceImpl<PointsMallOrderSubHistory> implements PointsMallOrderSubHistoryService { public class PointsMallOrderSubHistoryServiceImpl extends BaseServiceImpl<PointsMallOrderSubHistory> implements PointsMallOrderSubHistoryService {
@Inject
private MallInvoiceInfoQueryService mallInvoiceInfoQueryService;
@Inject
private SysFileService sysFileService;
public PointsMallOrderSubHistoryServiceImpl(Dao dao) { public PointsMallOrderSubHistoryServiceImpl(Dao dao) {
super(dao); super(dao);
} }
@Override @Override
public List<OrderProInfoDTO> getPictureBase64(String supplierId, String orderId, String queryType, List<OrderProInfoDTO> orderProInfo) { public List<OrderProInfoDTO> getPictureBase64(String supplierId, String orderId, String queryType, List<OrderProInfoDTO> orderProInfo) {
return CollUtil.isEmpty(orderProInfo) ? Collections.emptyList() : orderProInfo; if (CollUtil.isEmpty(orderProInfo)) {
return Collections.emptyList();
}
List<String> fileIds = orderProInfo.stream().map(OrderProInfoDTO::getSku)
.filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList());
if (fileIds.isEmpty()) {
throw new IllegalArgumentException("商品SKU为空,无法获取商品图片");
}
try {
// 供应商按子订单和 SKU 返回图片 Base64,订单只保存工会文件服务地址。
MallPictureOutboundResult result = mallInvoiceInfoQueryService.queryFiles(supplierId, orderId, queryType, fileIds);
Map<String, String> localUrls = new LinkedHashMap<>();
for (OrderProInfoDTO product : orderProInfo) {
String sku = product.getSku();
if (StrUtil.isBlank(sku)) {
throw new IllegalArgumentException("商品SKU为空,无法获取商品图片");
}
String localUrl = localUrls.get(sku);
if (localUrl == null) {
String base64 = result.getImageEncodeMap() == null ? null : result.getImageEncodeMap().get(sku);
if (StrUtil.isBlank(base64)) {
if (CollUtil.isEmpty(product.getPicInfo()) && StrUtil.isBlank(product.getFilePath()) && StrUtil.isBlank(product.getFileId())) {
continue;
}
throw new IllegalArgumentException("未获取到商品图片Base64" + sku);
}
if (base64.startsWith("data:")) {
base64 = base64.substring(base64.indexOf(',') + 1);
}
byte[] bytes = Base64.getDecoder().decode(base64.replaceAll("\\s", ""));
String fileType = FileTypeUtil.getType(new ByteArrayInputStream(bytes));
if (bytes.length == 0 || fileType == null || !fileType.matches("(?i)jpg|jpeg|png|gif|bmp|webp")) {
throw new IllegalArgumentException("商品图片内容为空或格式不支持:" + sku);
}
localUrl = sysFileService.uploadReturnUrl(SysFileEngineTypeEnum.MINIO.getValue(),
"product-" + UUID.randomUUID() + "." + fileType.toLowerCase(java.util.Locale.ROOT), bytes);
if (StrUtil.isBlank(localUrl)) {
throw new IllegalArgumentException("商品图片上传失败:" + sku);
}
localUrls.put(sku, localUrl);
}
product.setPicInfo(Collections.singletonList(localUrl));
product.setFilePath(localUrl);
product.setFileId(localUrl);
}
return orderProInfo;
} catch (Exception e) {
throw new IllegalArgumentException("商品图片同步失败:" + e.getMessage(), e);
}
} }
@Override @Override
@@ -48,15 +48,11 @@ public class PointsMallReconciliationController {
} }
} }
@At @At("/execute")
@SLog(tag = "积分商城对账管理", msg = "执行订单对账") @SLog(tag = "积分商城对账管理", msg = "执行订单对账")
@SaCheckPermission("points.mall.reconciliation") @SaCheckPermission("points.mall.reconciliation")
public Result execute(PointsMallReconciliationParam param) { public Result execute(PointsMallReconciliationParam param) {
try { return pointsMallReconciliationService.executeReconciliation(param);
return Result.success(pointsMallReconciliationService.execute(param));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
} }
@At @At
@@ -8,6 +8,10 @@ public class PointsMallReconciliationParam {
private String supplierId; private String supplierId;
private Integer reconciliationType; private Integer reconciliationType;
private String orderId; private String orderId;
private String queryOrderId;
private String userId;
private Integer reconciliationStatus;
private Integer invoiceStatus;
private String startDate; private String startDate;
private String endDate; private String endDate;
} }
@@ -1,6 +1,7 @@
package com.budwk.app.zhgh.pointsmall.reconciliation.service; package com.budwk.app.zhgh.pointsmall.reconciliation.service;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.BaseService; import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.pointsmall.reconciliation.models.PointsMallReconciliationRecord; import com.budwk.app.zhgh.pointsmall.reconciliation.models.PointsMallReconciliationRecord;
import com.budwk.app.zhgh.pointsmall.reconciliation.param.PointsMallReconciliationOrderPageParam; import com.budwk.app.zhgh.pointsmall.reconciliation.param.PointsMallReconciliationOrderPageParam;
@@ -18,7 +19,7 @@ public interface PointsMallReconciliationService extends BaseService<PointsMallR
BigDecimal pointsTotal(PointsMallReconciliationParam param); BigDecimal pointsTotal(PointsMallReconciliationParam param);
String execute(PointsMallReconciliationParam param); Result executeReconciliation(PointsMallReconciliationParam param);
Pagination<Record> recordPage(PointsMallReconciliationRecordPageParam param); Pagination<Record> recordPage(PointsMallReconciliationRecordPageParam param);
@@ -2,35 +2,66 @@ package com.budwk.app.zhgh.pointsmall.reconciliation.service.impl;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.OrderInfoByReconciliationDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.OrderProInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.QueryReconciliationInfoDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.ReconciliationResultNoticeDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.ReconciliationResultOrderDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.UnReconciledMsgNoticeDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallReconciliationInfoQueryService;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallReconciliationResultNoticeService;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallUnReconciledMsgNoticeService;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub; import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub;
import com.budwk.app.zhgh.pointsmall.reconciliation.models.PointsMallReconciliationOrder;
import com.budwk.app.zhgh.pointsmall.reconciliation.models.PointsMallReconciliationRecord; import com.budwk.app.zhgh.pointsmall.reconciliation.models.PointsMallReconciliationRecord;
import com.budwk.app.zhgh.pointsmall.reconciliation.param.PointsMallReconciliationOrderPageParam; import com.budwk.app.zhgh.pointsmall.reconciliation.param.PointsMallReconciliationOrderPageParam;
import com.budwk.app.zhgh.pointsmall.reconciliation.param.PointsMallReconciliationParam; import com.budwk.app.zhgh.pointsmall.reconciliation.param.PointsMallReconciliationParam;
import com.budwk.app.zhgh.pointsmall.reconciliation.param.PointsMallReconciliationRecordPageParam; import com.budwk.app.zhgh.pointsmall.reconciliation.param.PointsMallReconciliationRecordPageParam;
import com.budwk.app.zhgh.pointsmall.reconciliation.service.PointsMallReconciliationService; import com.budwk.app.zhgh.pointsmall.reconciliation.service.PointsMallReconciliationService;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier; import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Chain; import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.entity.Record; import org.nutz.dao.entity.Record;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 对账功能
*/
@Slf4j
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class PointsMallReconciliationServiceImpl extends BaseServiceImpl<PointsMallReconciliationRecord> implements PointsMallReconciliationService { public class PointsMallReconciliationServiceImpl extends BaseServiceImpl<PointsMallReconciliationRecord> implements PointsMallReconciliationService {
@Inject
private MallReconciliationInfoQueryService mallReconciliationInfoQueryService;
@Inject
private MallUnReconciledMsgNoticeService unReconciledMsgNoticeService;
@Inject
private MallReconciliationResultNoticeService reconciliationResultNoticeService;
public PointsMallReconciliationServiceImpl(Dao dao) { public PointsMallReconciliationServiceImpl(Dao dao) {
super(dao); super(dao);
} }
@@ -53,37 +84,148 @@ public class PointsMallReconciliationServiceImpl extends BaseServiceImpl<PointsM
return record == null || record.get("totalPoints") == null ? BigDecimal.ZERO : new BigDecimal(record.get("totalPoints").toString()); return record == null || record.get("totalPoints") == null ? BigDecimal.ZERO : new BigDecimal(record.get("totalPoints").toString());
} }
/**
* 执行对账
*
* @param reconciliationBO 入参
*/
@Override @Override
public String execute(PointsMallReconciliationParam param) { public Result executeReconciliation(PointsMallReconciliationParam reconciliationBO) {
checkReconciliationParam(param); try {
List<PointsMallOrderSub> orders = dao().query(PointsMallOrderSub.class, reconciliationCnd(param, "")); checkReconciliationParam(reconciliationBO);
if (CollUtil.isEmpty(orders)) { } catch (IllegalArgumentException e) {
return "未查询到待对账订单"; return Result.error(e.getMessage());
}
Integer reconciliationType = reconciliationBO.getReconciliationType();
// 从积分商城订单表查询对账订单
List<PointsMallOrderSub> orderInfoList = dao().query(PointsMallOrderSub.class, reconciliationCnd(reconciliationBO, ""));
if (CollUtil.isEmpty(orderInfoList)) {
return Result.error("积分商城没有查询到对账订单数据");
}
Set<String> orderIdList = orderInfoList.stream().map(PointsMallOrderSub::getOrderId).collect(Collectors.toSet());
if (StrUtil.isNotBlank(reconciliationBO.getOrderId())
&& !orderIdList.containsAll(StrUtil.splitTrim(reconciliationBO.getOrderId(), ","))) {
return Result.error("部分勾选订单已不符合对账条件,请重新查询后选择");
} }
String batchNo = DateUtil.format(new Date(), "yyyyMMddHHmmss") + IdUtil.fastSimpleUUID().substring(0, 6); QueryReconciliationInfoDTO queryReconciliationInfoDTO = new QueryReconciliationInfoDTO();
int success = 0; queryReconciliationInfoDTO.setSupplierId(reconciliationBO.getSupplierId());
int exception = 0; queryReconciliationInfoDTO.setReconciliationType(reconciliationType);
List<PointsMallReconciliationRecord> records = new ArrayList<>(); queryReconciliationInfoDTO.setOrderId(String.join(",", orderIdList));
queryReconciliationInfoDTO.setStartDate(reconciliationBO.getStartDate());
for (PointsMallOrderSub order : orders) { queryReconciliationInfoDTO.setEndDate(reconciliationBO.getEndDate());
PointsMallReconciliationOrder supplierOrder = dao().fetch(PointsMallReconciliationOrder.class, queryReconciliationInfoDTO.setPage(1);
Cnd.where("orderId", "=", order.getOrderId()) queryReconciliationInfoDTO.setPageSize(100);
.and("supplierId", "=", order.getSupplierId()) // 通过对账接口从供应商拉取对账订单数据
.and("delFlag", "=", false)); List<OrderInfoByReconciliationDTO> result = mallReconciliationInfoQueryService.queryReconciliationInfo(queryReconciliationInfoDTO);
PointsMallReconciliationRecord record = buildRecord(order, supplierOrder, batchNo); if (CollUtil.isEmpty(result)) {
if (record.getDiffType() == null || record.getDiffType() == 0) { return Result.error("供应商侧没有拉取到对账订单数据");
success++;
dao().update(PointsMallOrderSub.class, Chain.make("reconciliationStatus", 1), Cnd.where("id", "=", order.getId()));
} else {
exception++;
dao().update(PointsMallOrderSub.class, Chain.make("reconciliationStatus", 2), Cnd.where("id", "=", order.getId()));
} }
records.add(record); result = result.stream().filter(o -> orderIdList.contains(o.getOrderId())).collect(Collectors.toList());
String supplierId = reconciliationBO.getSupplierId();
result.forEach(o -> o.setSupplierId(supplierId));
// 根据供应商ID + 订单ID进行分组
Map<String, List<OrderInfoByReconciliationDTO>> supplierOrderList = result.stream()
.collect(Collectors.groupingBy(o -> String.join("-", o.getSupplierId(), o.getOrderId())));
// 根据供应商ID + 订单ID进行分组
Map<String, List<PointsMallOrderSub>> pfOrderList = orderInfoList.stream()
.collect(Collectors.groupingBy(o -> String.join("-", o.getSupplierId(), o.getOrderId())));
Set<String> pfKeySet = pfOrderList.keySet();
Set<String> supplierKeySet = supplierOrderList.keySet();
// 浦发商城和供应商都存在的订单
Set<String> sameKeySet = pfKeySet.stream().filter(supplierKeySet::contains).collect(Collectors.toSet());
// 1.浦发商城和供应商两边的供应商ID + 订单ID可以匹配上
List<PointsMallReconciliationRecord> reconciliationList1 = getReconciliationResult(sameKeySet, pfOrderList, supplierOrderList);
if (CollUtil.isNotEmpty(reconciliationList1)) {
List<PointsMallReconciliationRecord> reconciliationList0 = reconciliationList1.stream()
.filter(o -> o.getDiffType() == 3).collect(Collectors.toList());
if (CollUtil.isNotEmpty(reconciliationList0)) {
UnReconciledMsgNoticeDTO noticeDTO = getUnReconciledMsgNoticeDTO(reconciliationBO, reconciliationList0);
noticeDTO.setOrderIds(reconciliationList0.stream().map(PointsMallReconciliationRecord::getOrderId).collect(Collectors.joining(",")));
// 更新子订单状态为对账异常
subOrderReconciliationUpdate(reconciliationList0, 2);
// 对账异常结果通知接口
unReconciledMsgNoticeService.unReconciledMsgNotice(noticeDTO);
} }
dao().insert(records); List<PointsMallReconciliationRecord> reconciliationSuccessList = reconciliationList1.stream()
return "对账完成,批次号:" + batchNo + ",成功:" + success + ",异常:" + exception; .filter(o -> o.getDiffType() == 0).collect(Collectors.toList());
if (CollUtil.isNotEmpty(reconciliationSuccessList)) {
// 更新子订单状态为对账完成
subOrderReconciliationUpdate(reconciliationSuccessList, 1);
}
}
// 2.积分商城已确认收货对账接口未返回
List<PointsMallReconciliationRecord> reconciliationList2 = orderNotReturn(pfKeySet, supplierKeySet, pfOrderList);
if (CollUtil.isNotEmpty(reconciliationList2)) {
reconciliationList1.addAll(reconciliationList2);
UnReconciledMsgNoticeDTO noticeDTO = getUnReconciledMsgNoticeDTO(reconciliationBO, reconciliationList2);
noticeDTO.setOrderIds(reconciliationList2.stream().map(PointsMallReconciliationRecord::getOrderId).collect(Collectors.joining(",")));
// 更新子订单状态为对账异常
subOrderReconciliationUpdate(reconciliationList2, 2);
// 对账异常结果通知接口
unReconciledMsgNoticeService.unReconciledMsgNotice(noticeDTO);
}
// 3.积分商城不存在此订单无法对账
List<PointsMallReconciliationRecord> reconciliationList3 = orderNotExist(supplierKeySet, pfKeySet, supplierOrderList);
if (CollUtil.isNotEmpty(reconciliationList3)) {
reconciliationList1.addAll(reconciliationList3);
UnReconciledMsgNoticeDTO noticeDTO = getUnReconciledMsgNoticeDTO(reconciliationBO, reconciliationList3);
noticeDTO.setOrderIds(reconciliationList3.stream().map(PointsMallReconciliationRecord::getOrderId).collect(Collectors.joining(",")));
// 更新子订单状态为对账异常
// subOrderReconciliationUpdate(reconciliationList3, 2);
// 对账异常结果通知接口
unReconciledMsgNoticeService.unReconciledMsgNotice(noticeDTO);
}
if (CollUtil.isNotEmpty(reconciliationList1)) {
String startDate = reconciliationBO.getStartDate();
String endDate = reconciliationBO.getEndDate();
String startEndDate = StrUtil.blankToDefault(startDate, "history").replace("-", "")
+ StrUtil.blankToDefault(endDate, "").replace("-", "");
String loginName = StrUtil.blankToDefault(SecurityUtil.getUserId(), "console");
LocalDateTime now = LocalDateTime.now();
String yyyyMMddHHmm = now.format(DateTimeFormatter.ofPattern("yyyyMMddHHmm"));
reconciliationList1.forEach(o ->
o.setBatchNo(String.format("%s-%s-%s", startEndDate, loginName, yyyyMMddHHmm)));
// 把对账结果写入数据库
dao().insert(reconciliationList1);
ReconciliationResultNoticeDTO resultNoticeDTO = new ReconciliationResultNoticeDTO();
resultNoticeDTO.setSupplierId(reconciliationBO.getSupplierId());
resultNoticeDTO.setReconciliationBatchNo(reconciliationList1.get(0).getBatchNo());
resultNoticeDTO.setReconciliationTime(now.toString());
resultNoticeDTO.setReconciliationResults(reconciliationList1.stream().map(o -> {
ReconciliationResultOrderDTO resultOrderDTO = new ReconciliationResultOrderDTO();
resultOrderDTO.setOrderId(o.getOrderId());
if (StrUtil.isNotBlank(o.getUserId())) {
Sys_user user = dao().fetch(Sys_user.class, o.getUserId());
if (user == null || StrUtil.isBlank(user.getVirtualId())) {
throw new IllegalArgumentException("用户虚拟ID未配置或不存在");
}
resultOrderDTO.setUserId(user.getVirtualId());
}
resultOrderDTO.setReconciliationStatus(o.getDiffType() == 0 ? 1 : 2);
resultOrderDTO.setDiffType(o.getDiffType());
resultOrderDTO.setDiffReason(o.getDiffReason());
resultOrderDTO.setPlatformTotalPrice(o.getPTotalPrice());
resultOrderDTO.setPlatformCashAmount(o.getPCashAmount());
resultOrderDTO.setPlatformPointsPrice(o.getPPointsPrice());
resultOrderDTO.setPlatformRefundPrice(o.getPRefund());
resultOrderDTO.setSupplierTotalPrice(o.getSTotalPrice());
resultOrderDTO.setSupplierCashAmount(o.getSCashAmount());
resultOrderDTO.setSupplierPointsPrice(o.getSPointsPrice());
resultOrderDTO.setSupplierRefundPrice(o.getSRefund());
return resultOrderDTO;
}).collect(Collectors.toList()));
reconciliationResultNoticeService.reconciliationResultNotice(resultNoticeDTO);
}
return Result.success();
} }
@Override @Override
@@ -93,21 +235,21 @@ public class PointsMallReconciliationServiceImpl extends BaseServiceImpl<PointsM
cnd.and("r.supplierId", "=", param.getSupplierId()); cnd.and("r.supplierId", "=", param.getSupplierId());
} }
if (StrUtil.isNotBlank(param.getOrderId())) { if (StrUtil.isNotBlank(param.getOrderId())) {
cnd.and("r.orderId", "like", "%" + param.getOrderId() + "%"); cnd.and("r.orderId", "=", param.getOrderId());
} }
if (StrUtil.isNotBlank(param.getUserId())) { if (StrUtil.isNotBlank(param.getUserId())) {
cnd.and("r.userId", "like", "%" + param.getUserId() + "%"); cnd.and("r.userId", "=", param.getUserId());
} }
if (param.getDiffType() != null) { if (param.getDiffType() != null) {
cnd.and("r.diffType", "=", param.getDiffType()); cnd.and("r.diffType", "=", param.getDiffType());
} }
if (StrUtil.isNotBlank(param.getBatchNo())) { if (StrUtil.isNotBlank(param.getBatchNo())) {
cnd.and("r.batchNo", "like", "%" + param.getBatchNo() + "%"); cnd.and("r.batchNo", "=", param.getBatchNo());
} }
if (param.getNoticeStatus() != null) { if (param.getNoticeStatus() != null) {
cnd.and("r.noticeStatus", "=", param.getNoticeStatus()); cnd.and("r.noticeStatus", "=", param.getNoticeStatus());
} }
cnd.desc("r.createdAt"); cnd.desc("r.batchNo").desc("r.createdAt");
Sql sql = Sqls.create("select r.*, s.supplierName from points_mall_reconciliation_record r left join points_mall_supplier s on r.supplierId = s.supplierId and s.delFlag = 0 $condition"); Sql sql = Sqls.create("select r.*, s.supplierName from points_mall_reconciliation_record r left join points_mall_supplier s on r.supplierId = s.supplierId and s.delFlag = 0 $condition");
sql.setCondition(cnd); sql.setCondition(cnd);
return listPageMap(param.getPageNumber(), param.getPageSize(), sql); return listPageMap(param.getPageNumber(), param.getPageSize(), sql);
@@ -119,7 +261,8 @@ public class PointsMallReconciliationServiceImpl extends BaseServiceImpl<PointsM
} }
private Cnd orderCnd(PointsMallReconciliationOrderPageParam param, String alias) { private Cnd orderCnd(PointsMallReconciliationOrderPageParam param, String alias) {
Cnd cnd = Cnd.where(field(alias, "delFlag"), "=", false); Cnd cnd = Cnd.where(field(alias, "delFlag"), "=", false)
.and(field(alias, "orderState"), "=", 5);
if (StrUtil.isNotBlank(param.getSupplierId())) { if (StrUtil.isNotBlank(param.getSupplierId())) {
cnd.and(field(alias, "supplierId"), "=", param.getSupplierId()); cnd.and(field(alias, "supplierId"), "=", param.getSupplierId());
} }
@@ -145,22 +288,23 @@ public class PointsMallReconciliationServiceImpl extends BaseServiceImpl<PointsM
} }
private Cnd reconciliationCnd(PointsMallReconciliationParam param, String alias) { private Cnd reconciliationCnd(PointsMallReconciliationParam param, String alias) {
Cnd cnd = Cnd.where(field(alias, "delFlag"), "=", false); PointsMallReconciliationOrderPageParam query = new PointsMallReconciliationOrderPageParam();
cnd.and(field(alias, "supplierId"), "=", param.getSupplierId()); query.setSupplierId(param.getSupplierId());
query.setOrderId(param.getQueryOrderId());
query.setUserId(param.getUserId());
query.setReconciliationStatus(param.getReconciliationStatus());
query.setInvoiceStatus(param.getInvoiceStatus());
query.setStartDate(param.getStartDate());
query.setEndDate(param.getEndDate());
Cnd cnd = orderCnd(query, alias);
if (param.getReconciliationType() != null && param.getReconciliationType() == 2) { if (param.getReconciliationType() != null && param.getReconciliationType() == 2) {
cnd.and(field(alias, "reconciliationStatus"), "=", 2); cnd.and(field(alias, "reconciliationStatus"), "=", 2);
} else { } else {
cnd.and(field(alias, "reconciliationStatus"), "=", 0); cnd.and(field(alias, "reconciliationStatus"), "=", 0);
} }
if (StrUtil.isNotBlank(param.getOrderId())) { if (StrUtil.isNotBlank(param.getOrderId())) {
List<String> orderIds = StrUtil.splitTrim(param.getOrderId(), ","); cnd.and(field(alias, "orderId"), "in", StrUtil.splitTrim(param.getOrderId(), ","));
if (orderIds.size() > 1) {
cnd.and(field(alias, "orderId"), "in", orderIds);
} else {
cnd.and(field(alias, "orderId"), "like", "%" + param.getOrderId() + "%");
} }
}
addDateRange(cnd, alias, param.getStartDate(), param.getEndDate());
return cnd; return cnd;
} }
@@ -190,56 +334,233 @@ public class PointsMallReconciliationServiceImpl extends BaseServiceImpl<PointsM
if (StrUtil.isBlank(param.getStartDate()) || StrUtil.isBlank(param.getEndDate())) { if (StrUtil.isBlank(param.getStartDate()) || StrUtil.isBlank(param.getEndDate())) {
throw new IllegalArgumentException("对账日期不能为空"); throw new IllegalArgumentException("对账日期不能为空");
} }
if (param.getReconciliationType() != 1 && param.getReconciliationType() != 2) {
throw new IllegalArgumentException("对账类型不正确");
}
} }
private PointsMallReconciliationRecord buildRecord(PointsMallOrderSub order, PointsMallReconciliationOrder supplierOrder, String batchNo) { private void subOrderReconciliationUpdate(List<PointsMallReconciliationRecord> reconciliationList, Integer reconciliationStatus) {
PointsMallReconciliationRecord record = new PointsMallReconciliationRecord(); // 更新子订单状态为对账异常
record.setSupplierId(order.getSupplierId()); reconciliationList.forEach(o -> dao().update(PointsMallOrderSub.class,
record.setOrderId(order.getOrderId()); Chain.make("reconciliationStatus", reconciliationStatus),
record.setUserId(order.getUserId()); Cnd.where("orderId", "=", o.getOrderId()).and("supplierId", "=", o.getSupplierId())));
record.setPRefund(nullToZero(order.getRefund()));
record.setPTotalPrice(nullToZero(order.getTotalPrice()));
record.setPCashAmount(nullToZero(order.getWPayOrAPay()));
record.setPPointsPrice(nullToZero(order.getPointsPrice()));
record.setBatchNo(batchNo);
record.setNoticeStatus(0);
record.setPushTime(0);
if (supplierOrder == null) {
record.setSRefund(BigDecimal.ZERO);
record.setSTotalPrice(record.getPTotalPrice());
record.setSCashAmount(record.getPCashAmount());
record.setSPointsPrice(record.getPPointsPrice());
record.setDiffType(0);
record.setDiffReason("未获取到供应商侧对账订单,按平台订单确认");
return record;
} }
record.setSRefund(BigDecimal.ZERO); private UnReconciledMsgNoticeDTO getUnReconciledMsgNoticeDTO(PointsMallReconciliationParam reconciliationBO,
record.setSTotalPrice(nullToZero(supplierOrder.getTotalPrice())); List<PointsMallReconciliationRecord> reconciliationList2) {
record.setSCashAmount(BigDecimal.ZERO); UnReconciledMsgNoticeDTO noticeDTO = new UnReconciledMsgNoticeDTO();
record.setSPointsPrice(nullToZero(supplierOrder.getTotalPrice())); noticeDTO.setUniqueSeqNo("111");
noticeDTO.setToken("token");
List<String> reasons = new ArrayList<>(); noticeDTO.setSupplierId(reconciliationBO.getSupplierId());
if (!amountEquals(record.getPTotalPrice(), record.getSTotalPrice())) { noticeDTO.setStartDate(reconciliationBO.getStartDate());
reasons.add("订单总金额不一致"); noticeDTO.setEndDate(reconciliationBO.getEndDate());
} PointsMallReconciliationRecord reconciliation = reconciliationList2.get(0);
if (!amountEquals(record.getPPointsPrice(), record.getSPointsPrice())) { noticeDTO.setUnReconciledType(reconciliation.getDiffType() + "");
reasons.add("积分金额不一致"); noticeDTO.setUnReconciledMsg(reconciliation.getDiffReason());
} return noticeDTO;
if (!Objects.equals(order.getUserId(), supplierOrder.getUserId())) {
reasons.add("用户ID不一致");
}
record.setDiffType(reasons.isEmpty() ? 0 : 1);
record.setDiffReason(reasons.isEmpty() ? "对账一致" : String.join("", reasons));
return record;
} }
private BigDecimal nullToZero(BigDecimal value) { /**
return value == null ? BigDecimal.ZERO : value; * 积分商城已确认收货对账接口未返回
*
* @param pfKeySet 浦发key
* @param supplierKeySet 供应商key
* @param pfOrderList 浦发商城对账订单
* @return 异常订单
*/
private List<PointsMallReconciliationRecord> orderNotReturn(Set<String> pfKeySet, Set<String> supplierKeySet,
Map<String, List<PointsMallOrderSub>> pfOrderList) {
List<PointsMallReconciliationRecord> reconciliationList = new ArrayList<>();
pfKeySet.stream().filter(key -> !supplierKeySet.contains(key)).forEach(key -> {
List<PointsMallOrderSub> pfOrders = pfOrderList.get(key);
if (CollUtil.isNotEmpty(pfOrders)) {
reconciliationList.add(buildReconciliationRecord(null, pfOrders.get(0), 1, "积分商城已确认收货,对账接口未返回"));
}
});
return reconciliationList;
} }
private boolean amountEquals(BigDecimal left, BigDecimal right) { /**
return nullToZero(left).compareTo(nullToZero(right)) == 0; * 积分商城不存在此订单无法对账
*
* @param supplierKeySet 供应商key
* @param pfKeySet 浦发key
* @param supplierOrderList 供应商对账订单
* @return 异常订单
*/
private List<PointsMallReconciliationRecord> orderNotExist(Set<String> supplierKeySet, Set<String> pfKeySet,
Map<String, List<OrderInfoByReconciliationDTO>> supplierOrderList) {
List<PointsMallReconciliationRecord> reconciliationList = new ArrayList<>();
supplierKeySet.stream().filter(key -> !pfKeySet.contains(key)).forEach(key -> {
List<OrderInfoByReconciliationDTO> supplierOrders = supplierOrderList.get(key);
if (CollUtil.isNotEmpty(supplierOrders)) {
reconciliationList.add(buildReconciliationRecord(supplierOrders.get(0), null, 2, "积分商城不存在此订单,无法对账"));
}
});
return reconciliationList;
}
/**
* 浦发商城和供应商两边的供应商ID + 订单ID可以匹配上
*
* @param sameKeySet 双方都存在的订单ID
* @param pfOrderList 浦发商城的订单
* @param supplierOrderList 供应商的订单
* @return 对账结果
*/
private List<PointsMallReconciliationRecord> getReconciliationResult(Set<String> sameKeySet,
Map<String, List<PointsMallOrderSub>> pfOrderList,
Map<String, List<OrderInfoByReconciliationDTO>> supplierOrderList) {
List<PointsMallReconciliationRecord> reconciliationList = new ArrayList<>();
sameKeySet.forEach(key -> {
List<PointsMallOrderSub> pOrderList = pfOrderList.get(key);
List<OrderInfoByReconciliationDTO> sOrderList = supplierOrderList.get(key);
BigDecimal pfPointSum = pOrderList.stream()
.map(o -> (o.getPointsPrice() == null ? BigDecimal.ZERO : o.getPointsPrice())
.subtract(o.getRefund() == null ? BigDecimal.ZERO : o.getRefund()))
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal supplierPointSum = sOrderList.stream()
.map(OrderInfoByReconciliationDTO::getTotalPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 根据供应商ID和订单ID比对积分总金额是否一致
if (pfPointSum.compareTo(supplierPointSum) != 0) {
reconciliationList.add(buildReconciliationRecord(sOrderList.get(0), pOrderList.get(0), 3, "订单内容不一致,订单金额不一致"));
return;
}
if (CollUtil.isNotEmpty(pOrderList)) {
OrderInfoByReconciliationDTO sOrder = sOrderList.get(0);
PointsMallOrderSub pfOrder = pOrderList.get(0);
PointsMallReconciliationRecord reconciliation = buildReconciliationRecord(sOrder, pfOrder, 0, "无差异");
// 比对商品明细的sku商品数量商品价格商品税率是否一致
if (CollUtil.isNotEmpty(sOrderList)) {
String productInfo = pfOrder.getExtJson();
// 解析积分商品下的商品明细
List<OrderProInfoDTO> pfOrderProList = JSONUtil.toList(productInfo, OrderProInfoDTO.class);
// 处理商品名称价格税率的小数位不参与比较的字段
handleField(pfOrderProList);
// 从对账接口获取的订单商品
OrderInfoByReconciliationDTO supplierOrder = sOrderList.get(0);
List<OrderProInfoDTO> supplierOrderProList = supplierOrder.getOrderProInfos();
handleField(supplierOrderProList);
// 比较双方商品明细是否一致
if (!compareProduct(pfOrderProList, supplierOrderProList)) {
reconciliation.setDiffType(3);
reconciliation.setDiffReason("订单内容不一致,商品明细不一致");
}
}
reconciliationList.add(reconciliation);
}
});
return reconciliationList;
}
/**
* 处理商品价格和税率字段的小数位保证对比的时候小数位一致
*
* @param productList 订单下的商品集合
*/
private void handleField(List<OrderProInfoDTO> productList) {
// 商品名称不参与比较价格和税率小数点位数处理
productList.forEach(o -> {
o.setName(null);
if (o.getPrice() != null) {
o.setPrice(o.getPrice().setScale(2, RoundingMode.HALF_UP));
}
if (o.getTaxRate() != null) {
o.setTaxRate(o.getTaxRate().setScale(6, RoundingMode.HALF_UP));
}
});
}
/**
* 比较两个list<bean>是否完全一致不考虑顺序
*
* @param list1 集合1
* @param list2 集合2
* @return 是否相同
*/
private boolean compareProduct(List<OrderProInfoDTO> list1, List<OrderProInfoDTO> list2) {
// 1. 如果两个列表是同一个引用直接返回 true
if (list1 == list2) {
return true;
}
// 2. 如果其中有一个为 null或者商品总数不一致直接返回 false
if (list1 == null || list2 == null || list1.size() != list2.size()) {
return false;
}
// 3. 遍历第一个订单的商品检查是否都能在第二个订单中找到匹配的商品
for (OrderProInfoDTO p1 : list1) {
boolean foundMatch = false;
for (OrderProInfoDTO p2 : list2) {
// 通过sku+number+price+taxRate相同判定为商品信息一致
if (Objects.equals(p1.getSku(), p2.getSku()) &&
Objects.equals(p1.getNumber(), p2.getNumber()) &&
compareBigDecimal(p1.getPrice(), p2.getPrice()) &&
compareBigDecimal(p1.getTaxRate(), p2.getTaxRate())) {
foundMatch = true;
break;
}
}
if (!foundMatch) {
return false;
}
}
return true;
}
private boolean compareBigDecimal(BigDecimal a, BigDecimal b) {
// 两者都为空视为相等
if (a == null && b == null) {
return true;
}
// 只有一个为空视为不相等
if (a == null || b == null) {
return false;
}
// 都不为空使用compareTo 比较数值
return a.compareTo(b) == 0;
}
/**
* 订单积分对账数据赋值
*
* @param sOrder 对账接口返回的订单对账数据
* @param pOrder 子订单表的订单数据
* @param diffType 异常类型
* @param diffReason 异常原因
* @return 对账结果
*/
private PointsMallReconciliationRecord buildReconciliationRecord(OrderInfoByReconciliationDTO sOrder,
PointsMallOrderSub pOrder,
int diffType, String diffReason) {
PointsMallReconciliationRecord reconciliation = new PointsMallReconciliationRecord();
reconciliation.setDiffType(diffType);
reconciliation.setDiffReason(diffReason);
reconciliation.setNoticeStatus(0);
reconciliation.setPushTime(0);
if (sOrder != null) {
// 对账接口返回数据
reconciliation.setSupplierId(sOrder.getSupplierId());
reconciliation.setOrderId(sOrder.getOrderId());
reconciliation.setSTotalPrice(sOrder.getTotalPrice());
reconciliation.setSPointsPrice(sOrder.getPointsPrice());
reconciliation.setSCashAmount(sOrder.getWPayOrAPay());
}
if (pOrder != null) {
// 订单表里数据
reconciliation.setSupplierId(pOrder.getSupplierId());
reconciliation.setOrderId(pOrder.getOrderId());
reconciliation.setUserId(pOrder.getUserId());
reconciliation.setPTotalPrice(pOrder.getTotalPrice());
reconciliation.setPPointsPrice(pOrder.getPointsPrice());
reconciliation.setPCashAmount(pOrder.getWPayOrAPay());
reconciliation.setPRefund(pOrder.getRefund());
}
return reconciliation;
} }
} }
@@ -5,13 +5,13 @@ import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.pointsmall.reconciliationrecord.models.PointsMallReconciliationRecord; import com.budwk.app.zhgh.pointsmall.reconciliationrecord.models.PointsMallReconciliationRecord;
import com.budwk.app.zhgh.pointsmall.reconciliationrecord.param.PointsMallReconciliationRecordPageParam; import com.budwk.app.zhgh.pointsmall.reconciliationrecord.param.PointsMallReconciliationRecordPageParam;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier; import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import org.nutz.dao.entity.Record;
import java.util.List; import java.util.List;
import java.util.Map;
public interface PointsMallReconciliationRecordService extends BaseService<PointsMallReconciliationRecord> { public interface PointsMallReconciliationRecordService extends BaseService<PointsMallReconciliationRecord> {
Pagination<Record> page(PointsMallReconciliationRecordPageParam param); Pagination<Map<String, Object>> page(PointsMallReconciliationRecordPageParam param);
List<PointsMallSupplier> listSuppliers(); List<PointsMallSupplier> listSuppliers();
} }
@@ -3,6 +3,7 @@ package com.budwk.app.zhgh.pointsmall.reconciliationrecord.service.impl;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.pointsmall.reconciliationrecord.models.PointsMallReconciliationRecord; import com.budwk.app.zhgh.pointsmall.reconciliationrecord.models.PointsMallReconciliationRecord;
import com.budwk.app.zhgh.pointsmall.reconciliationrecord.param.PointsMallReconciliationRecordPageParam; import com.budwk.app.zhgh.pointsmall.reconciliationrecord.param.PointsMallReconciliationRecordPageParam;
import com.budwk.app.zhgh.pointsmall.reconciliationrecord.service.PointsMallReconciliationRecordService; import com.budwk.app.zhgh.pointsmall.reconciliationrecord.service.PointsMallReconciliationRecordService;
@@ -10,11 +11,14 @@ import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.entity.Record;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import java.util.List; import java.util.List;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class PointsMallReconciliationRecordServiceImpl extends BaseServiceImpl<PointsMallReconciliationRecord> implements PointsMallReconciliationRecordService { public class PointsMallReconciliationRecordServiceImpl extends BaseServiceImpl<PointsMallReconciliationRecord> implements PointsMallReconciliationRecordService {
@@ -24,30 +28,46 @@ public class PointsMallReconciliationRecordServiceImpl extends BaseServiceImpl<P
} }
@Override @Override
public Pagination<Record> page(PointsMallReconciliationRecordPageParam param) { public Pagination<Map<String, Object>> page(PointsMallReconciliationRecordPageParam param) {
Cnd cnd = Cnd.where("r.delFlag", "=", false); Cnd cnd = Cnd.where("r.delFlag", "=", false);
if (StrUtil.isNotBlank(param.getSupplierId())) { if (StrUtil.isNotBlank(param.getSupplierId())) {
cnd.and("r.supplierId", "=", param.getSupplierId()); cnd.and("r.supplierId", "=", param.getSupplierId());
} }
if (StrUtil.isNotBlank(param.getOrderId())) { if (StrUtil.isNotBlank(param.getOrderId())) {
cnd.and("r.orderId", "like", "%" + param.getOrderId() + "%"); cnd.and("r.orderId", "=", param.getOrderId());
} }
if (StrUtil.isNotBlank(param.getUserId())) { if (StrUtil.isNotBlank(param.getUserId())) {
cnd.and("r.userId", "like", "%" + param.getUserId() + "%"); List<Sys_user> users = dao().query(Sys_user.class, Cnd.where("loginname", "like", "%" + param.getUserId().trim() + "%")
.and("delFlag", "=", false));
if (users.isEmpty()) {
return new Pagination<>(param.getPageNumber(), param.getPageSize(), 0, Collections.emptyList());
}
cnd.and("r.userId", "in", users.stream().map(Sys_user::getId).collect(Collectors.toList()));
} }
if (param.getDiffType() != null) { if (param.getDiffType() != null) {
cnd.and("r.diffType", "=", param.getDiffType()); cnd.and("r.diffType", "=", param.getDiffType());
} }
if (StrUtil.isNotBlank(param.getBatchNo())) { if (StrUtil.isNotBlank(param.getBatchNo())) {
cnd.and("r.batchNo", "like", "%" + param.getBatchNo() + "%"); cnd.and("r.batchNo", "=", param.getBatchNo());
} }
if (param.getNoticeStatus() != null) { if (param.getNoticeStatus() != null) {
cnd.and("r.noticeStatus", "=", param.getNoticeStatus()); cnd.and("r.noticeStatus", "=", param.getNoticeStatus());
} }
cnd.desc("r.createdAt"); cnd.desc("r.batchNo").desc("r.createdAt");
Sql sql = Sqls.create("select r.*, s.supplierName from points_mall_reconciliation_record r left join points_mall_supplier s on r.supplierId = s.supplierId and s.delFlag = 0 $condition"); Sql sql = Sqls.create("select r.*, s.supplierName from points_mall_reconciliation_record r left join points_mall_supplier s on r.supplierId = s.supplierId and s.delFlag = 0 $condition");
sql.setCondition(cnd); sql.setCondition(cnd);
return listPageMap(param.getPageNumber(), param.getPageSize(), sql); Pagination<Map<String, Object>> page = listPageMap(param.getPageNumber(), param.getPageSize(), sql);
if (page.getList() != null && !page.getList().isEmpty()) {
List<String> userIds = page.getList().stream().map(record -> (String) record.get("userId"))
.filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList());
Map<String, String> loginNames = new HashMap<>();
if (!userIds.isEmpty()) {
List<Sys_user> users = dao().query(Sys_user.class, Cnd.where("id", "in", userIds).and("delFlag", "=", false));
users.forEach(user -> loginNames.put(user.getId(), user.getLoginname()));
}
page.getList().forEach(record -> record.put("loginname", loginNames.get(record.get("userId"))));
}
return page;
} }
@Override @Override
@@ -1,23 +1,36 @@
package com.budwk.app.zhgh.pointsmall.supplier.service.impl; package com.budwk.app.zhgh.pointsmall.supplier.service.impl;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier; import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import com.budwk.app.zhgh.pointsmall.supplier.param.PointsMallSupplierPageParam; import com.budwk.app.zhgh.pointsmall.supplier.param.PointsMallSupplierPageParam;
import com.budwk.app.zhgh.pointsmall.supplier.service.PointsMallSupplierService; import com.budwk.app.zhgh.pointsmall.supplier.service.PointsMallSupplierService;
import org.nutz.dao.Chain; import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.integration.jedis.RedisService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class PointsMallSupplierServiceImpl extends BaseServiceImpl<PointsMallSupplier> implements PointsMallSupplierService { public class PointsMallSupplierServiceImpl extends BaseServiceImpl<PointsMallSupplier> implements PointsMallSupplierService {
@Inject
private RedisService redisService;
public PointsMallSupplierServiceImpl(Dao dao) { public PointsMallSupplierServiceImpl(Dao dao) {
super(dao); super(dao);
} }
@@ -52,7 +65,32 @@ public class PointsMallSupplierServiceImpl extends BaseServiceImpl<PointsMallSup
cnd.and("mallType", "=", mallType); cnd.and("mallType", "=", mallType);
} }
cnd.asc("sortCode"); cnd.asc("sortCode");
return dao().query(PointsMallSupplier.class, cnd); List<PointsMallSupplier> list = dao().query(PointsMallSupplier.class, cnd);
if (Objects.equals(1, mallType)) {
Sys_user user = dao().fetch(Sys_user.class, SecurityUtil.getUserId());
if (user == null || StrUtil.isBlank(user.getVirtualId())) {
throw new BaseException("用户不存在");
}
for (PointsMallSupplier supplier : list) {
supplier.setSupplierUrl(getSupplierIndexUrl(user, supplier));
}
}
return list;
}
private String getSupplierIndexUrl(Sys_user user, PointsMallSupplier supplier) {
String supplierUrl = supplier.getSupplierUrl();
if (StrUtil.isBlank(supplierUrl) || !Objects.equals(1, supplier.getMallType())) {
return supplierUrl;
}
Map<String, String> params = new HashMap<>();
params.put("userId", user.getVirtualId());
if ("10101013".equals(supplier.getSupplierId())) {
params.put("orgId", user.getVirtualOrgId());
}
String keyword = IdUtil.fastSimpleUUID();
redisService.setex("supplier-session:" + keyword, 300, JSONUtil.toJsonStr(params));
return supplierUrl.concat(keyword);
} }
@Override @Override
@@ -127,6 +127,7 @@ public class CondolenceApplyController {
username as userName, username as userName,
loginname as loginName, loginname as loginName,
sex, sex,
mobile,
technicalTitle, technicalTitle,
IFNULL(unitname, '暂无') as unitName, IFNULL(unitname, '暂无') as unitName,
unitid as unitId, unitid as unitId,
@@ -145,9 +146,6 @@ public class CondolenceApplyController {
} }
if(!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { if(!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) {
if(AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_CHAIRMAN.name())) { if(AuthUtil.hasRoleOr(RoleConstant.BRANCH_UNION_ADMIN.name(), RoleConstant.BRANCH_UNION_CHAIRMAN.name())) {
if (StrUtil.isBlank(SecurityUtil.getUnionId())) {
return Result.success(java.util.Collections.emptyList());
}
cnd.and(View_user::getUnionId, "=", SecurityUtil.getUnionId()); cnd.and(View_user::getUnionId, "=", SecurityUtil.getUnionId());
} else { } else {
cnd.and(View_user::getId, "=", SecurityUtil.getUserId()); cnd.and(View_user::getId, "=", SecurityUtil.getUserId());
+8 -12
View File
@@ -16,20 +16,16 @@ sm4cbc:
keyHex: 00112233445566778899aabbccddeeff keyHex: 00112233445566778899aabbccddeeff
ivHex: 0102030405060708090a0b0c0d0e0f10 ivHex: 0102030405060708090a0b0c0d0e0f10
tomcat: jetty:
contextPath: "" contextPath: /
# 容器错误响应不披露版本;Session Cookie 保持 HttpOnly。 threadpool:
session: idleTimeout: 60000
cookie: minThreads: 10
httponly: true
executor:
maxIdleTime: 60000
minSpareThreads: 10
maxThreads: 500 maxThreads: 500
page: page:
403: /__container_error 403: /error/403.html
404: /__container_error 404: /error/404.html
500: /__container_error 500: /error/500.html
#结合ftp使用,或用nginx代理ftp路径 #结合ftp使用,或用nginx代理ftp路径
#staticPath: /Users/wizzer/temp/files #staticPath: /Users/wizzer/temp/files
#开发模式静态资源 #开发模式静态资源
+7
View File
@@ -510,3 +510,10 @@ CREATE TABLE IF NOT EXISTS `points_mall_schedule_user_points_record` (
KEY `idx_points_mall_schedule_user_points_user` (`userId`), KEY `idx_points_mall_schedule_user_points_user` (`userId`),
KEY `idx_points_mall_schedule_user_points_status` (`status`) KEY `idx_points_mall_schedule_user_points_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分商城定时发放积分记录'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分商城定时发放积分记录';
ALTER TABLE sys_user
ADD COLUMN virtual_id VARCHAR(64) NULL COMMENT '虚拟用户ID,积分商城向供应商传递的用户标识',
ADD COLUMN virtual_org_id VARCHAR(255) NULL COMMENT '虚拟组织ID';
UPDATE sys_user SET virtual_id = MD5(CONCAT(id, '_', loginName, '_', salt))
where id is not null and loginName is not null and salt is not null;
+2 -2
View File
@@ -14,7 +14,7 @@ var ioc={
// ( 2097152 2M) // ( 2097152 2M)
maxFileSize : 2097152, maxFileSize : 2097152,
// //
nameFilter : '(?i)^(.+[.])(gif|jpg|jpeg|png)$' nameFilter : '^(.+[.])(gif|jpg|png|svg)$'
} }
}, },
imageUpload : { imageUpload : {
@@ -32,7 +32,7 @@ var ioc={
// ( 209715200 200M) // ( 209715200 200M)
maxFileSize : 209715200, maxFileSize : 209715200,
// //
nameFilter : '(?i)^(.+[.])(gif|jpg|png|jpeg|doc|docx|xls|xlsx|rar|zip|7z|txt|pdf|pptx|ppt|mp4)$' nameFilter : '^(.+[.])(gif||svg|jpg|png|jpeg|doc|docx|xls|xlsx|rar|zip|7z|txt|pdf|pptx|ppt|mp4)$'
} }
}, },
fileUpload : { fileUpload : {
@@ -36,7 +36,7 @@ layout("/layouts/platform.html"){
新增场地 新增场地
</el-button> </el-button>
</table-tool> </table-tool>
<el-table v-loading="tableLoading" :data="tableData" @sort-change="pageOrder" :size="tableSize"> <el-table :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column> <el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column <el-table-column
v-for="column in tableColumns" v-for="column in tableColumns"
@@ -152,23 +152,11 @@ layout("/layouts/platform.html"){
.catch(() => {}) .catch(() => {})
}, },
switchChange(row) { switchChange(row) {
const previousState = !row.state this.$axios.post("/platform/siteCug/manage/submit", row).then((res) => {
this.tableLoading = true
this.$axios.post("/platform/siteCug/manage/switchState", {
id: row.id,
state: row.state
}).then((res) => {
if (res.code === 0) { if (res.code === 0) {
this.$message.success(res.msg) this.$message.success(res.msg)
} else { this.pageData()
// 状态更新失败时恢复开关,避免页面显示与数据库实际状态不一致。
this.$set(row, "state", previousState)
this.$message.warning(res.msg)
} }
}).catch(() => {
this.$set(row, "state", previousState)
}).finally(() => {
this.tableLoading = false
}) })
}, },
querySiteType() { querySiteType() {
@@ -187,6 +187,31 @@ layout("/layouts/platform.html"){
</el-form-item> </el-form-item>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="生日"
v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<el-form-item prop="condolenceBirthday" label="生日">
<el-input
:value="formData.condolenceBirthday && $moment(formData.condolenceBirthday).isValid() ?$moment(formData.condolenceBirthday).format('YYYY-MM-DD') : ''"
show-word-limit placeholder="请输入生日" disabled></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="身份证号"
v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<el-form-item prop="condolenceIdCard" label="身份证号">
<el-input v-model="formData.condolenceIdCard" show-word-limit
placeholder="请输入身份证号" disabled></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="联系方式"
v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<el-form-item prop="condolenceMobile" label="联系方式">
<el-input v-model="formData.condolenceMobile" show-word-limit
placeholder="请输入联系方式" disabled></el-input>
</el-form-item>
</el-descriptions-item>
<el-descriptions-item label="慰问类型" <el-descriptions-item label="慰问类型"
v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'"> v-if="formData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<el-form-item label="慰问类型" prop="condolenceTypeId"> <el-form-item label="慰问类型" prop="condolenceTypeId">
@@ -791,6 +816,7 @@ layout("/layouts/platform.html"){
trigger: ["change", "blur"] trigger: ["change", "blur"]
}, {pattern: /^([1-9]{1})(\d{15}|\d{18})$/, message: "请输入正确的银行卡号", trigger: "blur"}], }, {pattern: /^([1-9]{1})(\d{15}|\d{18})$/, message: "请输入正确的银行卡号", trigger: "blur"}],
bankOfDeposit: [{required: true, message: "请填写开户行", trigger: ["change", "blur"]}], bankOfDeposit: [{required: true, message: "请填写开户行", trigger: ["change", "blur"]}],
condolenceMobile: [{pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur"}],
condolenceTime: [{required: true, message: "请选择慰问时间", trigger: ["change", "blur"]}], condolenceTime: [{required: true, message: "请选择慰问时间", trigger: ["change", "blur"]}],
// invoiceNumber: [{required: true, message: "请填写发票张数", trigger: ["change", "blur"]}], // invoiceNumber: [{required: true, message: "请填写发票张数", trigger: ["change", "blur"]}],
// invoice: [{required: true, message: "请填写发票号码", trigger: ["change", "blur"]}], // invoice: [{required: true, message: "请填写发票号码", trigger: ["change", "blur"]}],
@@ -1161,7 +1187,10 @@ layout("/layouts/platform.html"){
unionId, unionId,
unionName, unionName,
unionCode, unionCode,
sex sex,
birthday,
idCard,
mobile
} = user } = user
this.$set(this.formData, "condolenceUserName", userName) this.$set(this.formData, "condolenceUserName", userName)
this.$set(this.formData, "condolenceLoginName", loginName) this.$set(this.formData, "condolenceLoginName", loginName)
@@ -1171,6 +1200,9 @@ layout("/layouts/platform.html"){
this.$set(this.formData, "condolenceUnionName", unionName) this.$set(this.formData, "condolenceUnionName", unionName)
this.$set(this.formData, "condolenceUnionCode", unionCode) this.$set(this.formData, "condolenceUnionCode", unionCode)
this.$set(this.formData, "condolenceSex", sex) this.$set(this.formData, "condolenceSex", sex)
this.$set(this.formData, "condolenceBirthday", birthday)
this.$set(this.formData, "condolenceIdCard", idCard)
this.$set(this.formData, "condolenceMobile", mobile)
} }
}, },
typeChange(val) { typeChange(val) {
@@ -86,6 +86,18 @@ const unionReimburseInfo = {
<span>{{ viewData.condolenceSex }}</span> <span>{{ viewData.condolenceSex }}</span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="生日" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<span>{{ viewData.condolenceBirthday && $moment(viewData.condolenceBirthday).isValid() ? $moment(viewData.condolenceBirthday).format('YYYY-MM-DD') : '' }}</span>
</el-descriptions-item>
<el-descriptions-item label="身份证号" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<span>{{ viewData.condolenceIdCard }}</span>
</el-descriptions-item>
<el-descriptions-item label="联系方式" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<span>{{ viewData.condolenceMobile }}</span>
</el-descriptions-item>
<el-descriptions-item label="慰问类型" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'"> <el-descriptions-item label="慰问类型" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<span>{{ viewData.typeName }}</span> <span>{{ viewData.typeName }}</span>
</el-descriptions-item> </el-descriptions-item>
@@ -109,27 +109,15 @@ layout("/layouts/platform.html"){
if (!value) return [] if (!value) return []
try { try {
const data = typeof value === "string" ? JSON.parse(value) : value const data = typeof value === "string" ? JSON.parse(value) : value
if (Array.isArray(data)) return data return Array.isArray(data) ? data : []
if (data && typeof data === "object") {
return Object.keys(data).map(item => ({item, result: data[item]}))
}
return []
} catch (e) { } catch (e) {
return [] return []
} }
}, },
voteOptionText(item) {
const value = item.result || item.vote || item.selected || item.option || item.value
if (Array.isArray(value)) return value.join("、")
if (value && typeof value === "object") {
return value.label || value.name || value.value || "-"
}
return value || "-"
},
voteResultText(value) { voteResultText(value) {
return this.parseVoteResult(value).map(item => { return this.parseVoteResult(value).map(item => {
return (item.item || item.itemName || "投票项") + "" + return (item.item || item.itemName || "投票项") + "" +
this.voteOptionText(item) (item.selected || item.option || item.value || "-")
}).join("") }).join("")
}, },
buildSummary(records) { buildSummary(records) {
@@ -137,7 +125,7 @@ layout("/layouts/platform.html"){
records.forEach(record => { records.forEach(record => {
this.parseVoteResult(record.voteResult).forEach(item => { this.parseVoteResult(record.voteResult).forEach(item => {
const itemName = item.item || item.itemName || "投票项" const itemName = item.item || item.itemName || "投票项"
const option = this.voteOptionText(item) const option = item.selected || item.option || item.value || "-"
if (!counter[itemName]) counter[itemName] = {} if (!counter[itemName]) counter[itemName] = {}
counter[itemName][option] = (counter[itemName][option] || 0) + 1 counter[itemName][option] = (counter[itemName][option] || 0) + 1
}) })
@@ -110,10 +110,10 @@ const PROPOSAL_INFO = {
</el-dialog> </el-dialog>
<!--审核信息支持按需同时展开多条记录初次加载和切换提案时仍保持全部折叠--> <!--审核信息支持按需同时展开多条记录初次加载和切换提案时仍保持全部折叠-->
<template v-if="displayDoneTasks.length"> <template v-if="doneTasks.length">
<div class="process-title">审核信息</div> <div class="process-title">审核信息</div>
<el-collapse v-model="activeTaskIds"> <el-collapse v-model="activeTaskIds">
<el-collapse-item v-for="task in displayDoneTasks" :key="task.id" :name="task.id"> <el-collapse-item v-for="task in doneTasks" :key="task.id" :name="task.id">
<template slot="title"> <template slot="title">
<!--折叠标题使用独立的单行布局避免复用 process-title 后产生外边距和高度冲突--> <!--折叠标题使用独立的单行布局避免复用 process-title 后产生外边距和高度冲突-->
<div style="display: flex;align-items: center;justify-content: space-between;width: 100%;min-width: 0;padding-left: 10px"> <div style="display: flex;align-items: center;justify-content: space-between;width: 100%;min-width: 0;padding-left: 10px">
@@ -125,28 +125,8 @@ const PROPOSAL_INFO = {
</span> </span>
</div> </div>
</template> </template>
<!--同一提案可能存在多个承办单位答复任务查看时合并为一个流程节点节点内保留每条答复-->
<template v-if="task.replyTasks">
<el-descriptions v-for="replyTask in task.replyTasks" border class="flow-task-form"
:column="3" :key="replyTask.id">
<el-descriptions-item label="承办单位" :span="3">
{{replyTask.ext.underTakeName}}
</el-descriptions-item>
<el-descriptions-item label="办理用户">
{{replyTask.taskFormData.userName}}({{replyTask.taskFormData.loginName}})
</el-descriptions-item>
<el-descriptions-item label="办理时间">{{replyTask.finishTime}}</el-descriptions-item>
<el-descriptions-item label="办理结果">
<dict-tag :options="dict.type.PROCESS_TASK_SUBMIT_TYPE"
:value="replyTask.ext.submitType"></dict-tag>
</el-descriptions-item>
<el-descriptions-item label="办理意见" :span="3">
<div v-html="replyTask.taskFormData.tf_opinion"></div>
</el-descriptions-item>
</el-descriptions>
</template>
<el-descriptions border class="flow-task-form" :column="3" :key="task.id" <el-descriptions border class="flow-task-form" :column="3" :key="task.id"
v-else-if="task.ext.isFirstTaskNode"> v-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">{{ task.ext.initiatorName <el-descriptions-item label="申请用户">{{ task.ext.initiatorName
}}({{task.ext.initiatorAccount}}) }}({{task.ext.initiatorAccount}})
</el-descriptions-item> </el-descriptions-item>
@@ -253,7 +233,6 @@ const PROPOSAL_INFO = {
tableLoading: false, tableLoading: false,
viewData: {}, viewData: {},
doneTasks: [], doneTasks: [],
displayDoneTasks: [],
activeTaskIds: [], activeTaskIds: [],
row: null, row: null,
viewDialogVisible: false viewDialogVisible: false
@@ -279,7 +258,6 @@ const PROPOSAL_INFO = {
this.$set(this, "tableLoading", true) this.$set(this, "tableLoading", true)
// 切换提案时先清空上一条提案的审核记录和展开状态,避免异步加载期间展示旧数据。 // 切换提案时先清空上一条提案的审核记录和展开状态,避免异步加载期间展示旧数据。
this.doneTasks = [] this.doneTasks = []
this.displayDoneTasks = []
this.activeTaskIds = [] this.activeTaskIds = []
let pendingRequestCount = 2 let pendingRequestCount = 2
const handleRequestComplete = () => { const handleRequestComplete = () => {
@@ -315,94 +293,15 @@ const PROPOSAL_INFO = {
return this.$axios.post("/flow/common/doneTasks", {bizId: this.row.id}).then((res) => { return this.$axios.post("/flow/common/doneTasks", {bizId: this.row.id}).then((res) => {
if (res.code === 0) { if (res.code === 0) {
const tasks = res.data || [] const tasks = res.data || []
// 提案历史按业务阶段展示,避免较晚完成的附议记录被排到团长审核之后。 this.doneTasks = tasks
const sortedTasks = this.sortDoneTasks(tasks)
this.$set(this, "doneTasks", sortedTasks)
// 参考历史系统的展示方式,将多个单位的答复收纳在一个流程节点内。
this.$set(this, "displayDoneTasks", this.groupReplyTasks(sortedTasks))
// 审核记录加载完成后默认全部折叠,由用户按需展开查看。 // 审核记录加载完成后默认全部折叠,由用户按需展开查看。
this.activeTaskIds = [] this.activeTaskIds = []
// 通知外层页面已办节点数据已加载完成,便于当前环节做表单回显。 // 通知外层页面已办节点数据已加载完成,便于当前环节做表单回显。
this.$emit("done-tasks", this.doneTasks) this.$emit("done-tasks", tasks)
} }
}) })
}, },
/**
* 按提案业务阶段对审核历史排序
*
* @param tasks doneTasks 接口返回的已办任务数组
* @return 排序后的新数组同一阶段按办理时间任务 ID 保持稳定顺序
*/
sortDoneTasks(tasks) {
return (tasks || []).map((task, index) => ({task, index})).sort((left, right) => {
const stageDiff = this.getTaskStageOrder(left.task) - this.getTaskStageOrder(right.task)
if (stageDiff !== 0) {
return stageDiff
}
const leftTime = String(left.task.finishTime || "")
const rightTime = String(right.task.finishTime || "")
if (leftTime !== rightTime) {
if (!leftTime) return 1
if (!rightTime) return -1
return leftTime.localeCompare(rightTime)
}
const idDiff = Number(left.task.id || 0) - Number(right.task.id || 0)
return idDiff !== 0 ? idDiff : left.index - right.index
}).map((item) => item.task)
},
/**
* 将同一提案的承办单位答复任务合并成一个展示节点
*
* @param tasks 已按业务阶段排好顺序的任务数组
* @return 仅用于查看页面的展示数组原始任务及每条答复内容均保存在 replyTasks
*/
groupReplyTasks(tasks) {
const replyTaskNames = ["opinion_unit_reply", "unit_reply", "two_unit_reply", "master_reply", "slave_reply"]
const replyTasks = (tasks || []).filter((task) => replyTaskNames.includes(task.taskName))
if (!replyTasks.length) {
return tasks || []
}
const firstReplyTask = replyTasks[0]
const replyTaskIds = new Set(replyTasks.map((task) => task.id))
return (tasks || []).reduce((displayTasks, task) => {
if (!replyTaskIds.has(task.id)) {
displayTasks.push(task)
} else if (task.id === firstReplyTask.id) {
displayTasks.push({
id: "reply-group-" + firstReplyTask.id,
taskName: "replyGroup",
displayName: "承办单位答复",
replyTasks: replyTasks
})
}
return displayTasks
}, [])
},
/**
* 获取提案任务所属的展示阶段
* 未单列的预审核委员审议确认承办单位和复核记录统一放在委员会立案阶段
* 答复后的校领导审批通过 legacyStage 标识排在承办单位答复之后
*
* @param task 单条已办任务
* @return 数字类型的阶段顺序
*/
getTaskStageOrder(task) {
const taskName = task.taskName
const legacyStage = task.ext ? task.ext.legacyStage : ""
if (taskName === "startTask") return 10
if (taskName === "invite") return 15
if (taskName === "second") return 20
if (taskName === "delegation") return 30
if (["opinion_unit_reply", "unit_reply", "two_unit_reply", "master_reply", "slave_reply"].includes(taskName)) return 50
if (taskName === "schoolLeader" && legacyStage === "replyLeaderAudit") return 55
if (taskName === "feedback") return 60
return 40
},
openChart(){ openChart(){
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId) this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
} }
@@ -179,17 +179,16 @@ layout("/layouts/platform.html"){
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<!-- 承办单位答复附件上传入口暂时停用,保留原配置便于后续恢复。 --> <el-form-item prop="tf_attachment" label="附件">
<!-- <el-form-item prop="tf_attachment" label="附件">--> <file-upload
<!-- <file-upload--> :value.sync="formData.tf_attachment"
<!-- :value.sync="formData.tf_attachment"--> upload_mode="drag"
<!-- upload_mode="drag"--> :upload_number="5"
<!-- :upload_number="5"--> upload_result_category="array"
<!-- upload_result_category="array"--> complete_result
<!-- complete_result--> upload_mode="file"
<!-- upload_mode="file"--> ></file-upload>
<!-- ></file-upload>--> </el-form-item>
<!-- </el-form-item>-->
<el-form-item label="答复内容" prop="tf_opinion" <el-form-item label="答复内容" prop="tf_opinion"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]"> :rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<text-editor v-model="formData.tf_opinion"></text-editor> <text-editor v-model="formData.tf_opinion"></text-editor>
@@ -14,19 +14,11 @@ layout("/layouts/platform.html"){
<el-option label="失败" :value="1"></el-option> <el-option label="失败" :value="1"></el-option>
</el-select> </el-select>
</search-item> </search-item>
<search-item label="成功订单">
<el-input clearable placeholder="请输入订单ID" v-model="pageForm.sucOrderIds"></el-input>
</search-item>
<search-item label="失败订单">
<el-input clearable placeholder="请输入订单ID" v-model="pageForm.failOrderIds"></el-input>
</search-item>
</search> </search>
</el-card> </el-card>
<el-card class="mt10" shadow="never"> <el-card class="mt10" shadow="never">
<table-tool label="发票管理"> <table-tool label="发票管理">
<el-button @click="openApplyDialog" size="mini" type="success">开票申请</el-button>
<el-button @click="openOrderDialog" size="mini" type="primary">可开票订单</el-button>
</table-tool> </table-tool>
<el-table :data="tableData" border class="vi-table" size="small" style="width: 100%" v-loading="tableLoading"> <el-table :data="tableData" border class="vi-table" size="small" style="width: 100%" v-loading="tableLoading">
<el-table-column :index="indexMethod" align="center" label="序号" type="index" width="70"></el-table-column> <el-table-column :index="indexMethod" align="center" label="序号" type="index" width="70"></el-table-column>
@@ -36,13 +28,11 @@ layout("/layouts/platform.html"){
<el-tag :type="invoiceResultType(row.bSuccess)" size="mini">{{ invoiceResultName(row.bSuccess) }}</el-tag> <el-tag :type="invoiceResultType(row.bSuccess)" size="mini">{{ invoiceResultName(row.bSuccess) }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="成功订单" min-width="220" prop="sucOrderIds" show-overflow-tooltip></el-table-column> <el-table-column align="center" fixed="right" label="操作" width="340">
<el-table-column align="center" label="失败订单" min-width="220" prop="failOrderIds" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="失败原因" min-width="220" prop="failMsg" show-overflow-tooltip></el-table-column>
<el-table-column align="center" fixed="right" label="操作" width="220">
<template slot-scope="{row}"> <template slot-scope="{row}">
<el-button @click="openInfoDialog(row)" size="mini" type="primary">发票明细</el-button> <el-button @click="openOrderDialog(row.sucOrderIds)" size="mini" type="primary">开票成功列表</el-button>
<el-button @click="openOrderDialog(row.sucOrderIds)" size="mini">成功订单</el-button> <el-button @click="openInfoDialog(row)" size="mini">查看</el-button>
<el-button v-if="Number(row.bSuccess) === 1" @click="retryInvoice(row.id)" :loading="retryLoading" size="mini" type="warning">重新获取发票</el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -58,120 +48,25 @@ layout("/layouts/platform.html"){
></el-pagination> ></el-pagination>
</el-card> </el-card>
<el-dialog :close-on-click-modal="false" title="开票申请" :visible.sync="applyDialogVisible" top="5vh" width="720px"> <el-dialog :close-on-click-modal="false" title="订单明细" :visible.sync="orderDialogVisible" top="5vh" width="90%">
<el-form :model="applyForm" label-width="120px" size="small"> <el-table :data="orderList" border size="small" style="width: 100%" v-loading="orderLoading">
<el-form-item label="供应商" required>
<el-select clearable filterable placeholder="请选择供应商" style="width: 100%" v-model="applyForm.supplierId">
<el-option :key="item.supplierId" :label="item.supplierName" :value="item.supplierId" v-for="item in supplierList"></el-option>
</el-select>
</el-form-item>
<el-form-item label="订单完成日期" required>
<el-date-picker
end-placeholder="结束日期"
range-separator="至"
start-placeholder="开始日期"
style="width: 100%"
type="daterange"
value-format="yyyy-MM-dd"
v-model="applyDateRange"
></el-date-picker>
</el-form-item>
<el-form-item label="订单ID" required>
<el-input placeholder="多个订单ID用英文逗号分隔" type="textarea" v-model="applyForm.orderIds"></el-input>
</el-form-item>
<el-form-item label="开票日期" required>
<el-date-picker placeholder="请选择开票日期" style="width: 100%" type="date" value-format="yyyy-MM-dd" v-model="applyForm.invoiceDate"></el-date-picker>
</el-form-item>
<el-form-item label="发票抬头">
<el-input clearable placeholder="请输入发票抬头" v-model="applyForm.invoiceTitle"></el-input>
</el-form-item>
<el-form-item label="纳税人识别号" required>
<el-input clearable placeholder="请输入纳税人识别号" v-model="applyForm.invoiceCode"></el-input>
</el-form-item>
<el-form-item label="发票地址" required>
<el-input clearable placeholder="请输入发票地址" v-model="applyForm.invoiceAddress"></el-input>
</el-form-item>
<el-form-item label="联系电话" required>
<el-input clearable placeholder="请输入联系电话" v-model="applyForm.invoiceContact"></el-input>
</el-form-item>
<el-form-item label="发票类型">
<el-select clearable placeholder="请选择发票类型" style="width: 100%" v-model="applyForm.invoiceType">
<el-option label="全电专票" :value="9"></el-option>
<el-option label="全电普票" :value="10"></el-option>
</el-select>
</el-form-item>
<el-form-item label="开票内容">
<el-input clearable placeholder="请输入开票内容" v-model="applyForm.invoiceContent"></el-input>
</el-form-item>
<el-form-item label="开户银行">
<el-input clearable placeholder="请输入开户银行" v-model="applyForm.bankName"></el-input>
</el-form-item>
<el-form-item label="银行账号">
<el-input clearable placeholder="请输入银行账号" v-model="applyForm.bankAccount"></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input placeholder="请输入备注" type="textarea" v-model="applyForm.remark"></el-input>
</el-form-item>
<el-form-item label="订单积分合计">
<span>{{ orderTotalPoints }}</span>
<el-button @click="loadOrderTotalPoints" size="mini" style="margin-left: 12px">计算</el-button>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="applyDialogVisible = false">取消</el-button>
<el-button @click="submitApply" :loading="applyLoading" type="primary">提交申请</el-button>
</span>
</el-dialog>
<el-dialog :close-on-click-modal="false" title="可开票订单" :visible.sync="orderDialogVisible" top="5vh" width="90%">
<el-form :inline="true" size="small" @submit.native.prevent>
<el-form-item label="供应商">
<el-select clearable filterable placeholder="供应商" style="width: 180px" v-model="orderForm.supplierId">
<el-option :key="item.supplierId" :label="item.supplierName" :value="item.supplierId" v-for="item in supplierList"></el-option>
</el-select>
</el-form-item>
<el-form-item label="订单ID">
<el-input clearable placeholder="多个订单ID用英文逗号分隔" v-model="orderForm.orderId"></el-input>
</el-form-item>
<el-form-item label="完成日期">
<el-date-picker
end-placeholder="结束日期"
range-separator="至"
start-placeholder="开始日期"
style="width: 260px"
type="daterange"
value-format="yyyy-MM-dd"
v-model="orderDateRange"
></el-date-picker>
</el-form-item>
<el-form-item label="开票状态">
<el-select clearable placeholder="开票状态" style="width: 130px" v-model="orderForm.invoiceStatus">
<el-option label="未开票" :value="0"></el-option>
<el-option label="已开票" :value="1"></el-option>
<el-option label="开票中" :value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="orderSearch" type="primary">查询</el-button>
<el-button @click="orderReset">重置</el-button>
<el-button @click="useOrdersForApply" type="success">用于开票</el-button>
</el-form-item>
</el-form>
<el-table :data="orderList" @selection-change="orderSelectionChange" border size="small" style="width: 100%" v-loading="orderLoading">
<el-table-column align="center" type="selection" width="50"></el-table-column>
<el-table-column :index="orderIndexMethod" align="center" label="序号" type="index" width="70"></el-table-column>
<el-table-column align="center" label="订单ID" min-width="170" prop="orderId" show-overflow-tooltip></el-table-column> <el-table-column align="center" label="订单ID" min-width="170" prop="orderId" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="主订单号" min-width="170" prop="mainOrderId" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="供应商" min-width="150" prop="supplierName" show-overflow-tooltip></el-table-column> <el-table-column align="center" label="供应商" min-width="150" prop="supplierName" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="用户ID" min-width="120" prop="userId" show-overflow-tooltip></el-table-column> <el-table-column align="center" label="订单状态" min-width="110">
<el-table-column align="center" label="完成时间" min-width="160" prop="orderCompleteTime"></el-table-column> <template slot-scope="{row}">
<el-table-column align="center" label="订单总金额" min-width="110" prop="totalPrice"></el-table-column> <el-tag :type="orderStatusType(row.orderState)" size="mini">{{ orderStateName(row.orderState) }}</el-tag>
<el-table-column align="center" label="积分金额" min-width="110" prop="pointsPrice"></el-table-column> </template>
<el-table-column align="center" label="退款积分" min-width="110" prop="refund"></el-table-column> </el-table-column>
<el-table-column align="center" label="票状态" min-width="100"> <el-table-column align="center" label="票状态" min-width="100">
<template slot-scope="{row}"> <template slot-scope="{row}">
<el-tag :type="invoiceStatusType(row.invoiceStatus)" size="mini">{{ invoiceStatusName(row.invoiceStatus) }}</el-tag> <el-tag :type="invoiceStatusType(row.invoiceStatus)" size="mini">{{ invoiceStatusName(row.invoiceStatus) }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="完成时间" min-width="160" prop="orderCompleteTime"></el-table-column>
<el-table-column align="center" label="实际消耗积分" min-width="110">
<template slot-scope="{row}">{{ (Number(row.pointsPrice || 0) - Number(row.refund || 0)).toFixed(2) }}</template>
</el-table-column>
</el-table> </el-table>
<el-pagination <el-pagination
:current-page="orderForm.pageNumber" :current-page="orderForm.pageNumber"
@@ -184,37 +79,30 @@ layout("/layouts/platform.html"){
style="margin-top: 16px; text-align: right" style="margin-top: 16px; text-align: right"
></el-pagination> ></el-pagination>
<span slot="footer"> <span slot="footer">
<span style="margin-right: 20px">总消耗积分:{{ orderTotalPoints }}</span>
<el-button @click="orderDialogVisible = false">关闭</el-button> <el-button @click="orderDialogVisible = false">关闭</el-button>
</span> </span>
</el-dialog> </el-dialog>
<el-dialog :close-on-click-modal="false" title="发票明细" :visible.sync="infoDialogVisible" top="5vh" width="90%"> <el-dialog :close-on-click-modal="false" title="发票明细列表" :visible.sync="infoDialogVisible" top="5vh" width="90%">
<el-form :inline="true" size="small" @submit.native.prevent> <el-table :data="infoList" stripe size="small" style="width: 100%" v-loading="infoLoading">
<el-form-item label="发票号码"> <el-table-column label="发票主ID" min-width="160" prop="mainId" show-overflow-tooltip></el-table-column>
<el-input clearable placeholder="发票号码" v-model="infoForm.invoiceId"></el-input> <el-table-column label="发票号码" min-width="170" prop="invoiceId" show-overflow-tooltip></el-table-column>
</el-form-item> <el-table-column label="发票代码" min-width="160" prop="invoiceCode" show-overflow-tooltip></el-table-column>
<el-form-item label="发票代码"> <el-table-column label="发票日期" min-width="140" prop="invoiceDate"></el-table-column>
<el-input clearable placeholder="发票代码" v-model="infoForm.invoiceCode"></el-input> <el-table-column label="发票裸价" min-width="130" prop="invoiceNakeAmount"></el-table-column>
</el-form-item> <el-table-column label="发票税率" min-width="110" prop="invoiceTaxRate"></el-table-column>
<el-form-item label="发票日期"> <el-table-column label="发票税额" min-width="130" prop="invoiceTaxAmount"></el-table-column>
<el-date-picker clearable placeholder="发票日期" type="date" value-format="yyyy-MM-dd" v-model="infoForm.invoiceDate"></el-date-picker> <el-table-column label="价税合计" min-width="130" prop="invoiceAmount"></el-table-column>
</el-form-item> <el-table-column label="发票类型" min-width="180">
<el-form-item> <template slot-scope="{row}"><el-tag size="mini">{{ Number(row.invoiceType) === 9 ? '全电发票(增值税专用发票)' : Number(row.invoiceType) === 10 ? '全电发票(增值税普通发票)' : invoiceTypeName(row.invoiceType) }}</el-tag></template>
<el-button @click="infoSearch" type="primary">查询</el-button> </el-table-column>
</el-form-item> <el-table-column align="center" label="电子发票" width="110">
</el-form> <template slot-scope="{row}"><el-button @click="previewInvoice(row)" size="mini" type="text">预览</el-button></template>
<el-table :data="infoList" border size="small" style="width: 100%" v-loading="infoLoading"> </el-table-column>
<el-table-column :index="infoIndexMethod" align="center" label="序号" type="index" width="70"></el-table-column> <el-table-column align="center" fixed="right" label="操作" width="100">
<el-table-column align="center" label="发票号码" min-width="150" prop="invoiceId" show-overflow-tooltip></el-table-column> <template slot-scope="{row}"><el-button @click="openInvoiceDetail(row)" size="mini" type="text">详情</el-button></template>
<el-table-column align="center" label="发票代码" min-width="130" prop="invoiceCode" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="发票日期" min-width="120" prop="invoiceDate"></el-table-column>
<el-table-column align="center" label="发票类型" min-width="110">
<template slot-scope="{row}">{{ invoiceTypeName(row.invoiceType) }}</template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="价税合计" min-width="110" prop="invoiceAmount"></el-table-column>
<el-table-column align="center" label="税额" min-width="100" prop="invoiceTaxAmount"></el-table-column>
<el-table-column align="center" label="纳税人识别号" min-width="160" prop="taxIdNumber" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="电子发票地址" min-width="220" prop="url" show-overflow-tooltip></el-table-column>
</el-table> </el-table>
<el-pagination <el-pagination
:current-page="infoForm.pageNumber" :current-page="infoForm.pageNumber"
@@ -225,9 +113,46 @@ layout("/layouts/platform.html"){
style="margin-top: 16px; text-align: right" style="margin-top: 16px; text-align: right"
></el-pagination> ></el-pagination>
<span slot="footer"> <span slot="footer">
<el-button @click="retryInvoice(infoForm.mainId)" :loading="retryLoading" type="primary">重新获取发票</el-button>
<el-button @click="infoDialogVisible = false">关闭</el-button> <el-button @click="infoDialogVisible = false">关闭</el-button>
</span> </span>
</el-dialog> </el-dialog>
<el-dialog :close-on-click-modal="false" title="发票详情" :visible.sync="detailDialogVisible" append-to-body top="5vh" width="70%">
<el-card v-loading="detailLoading" shadow="never">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="发票主ID">{{ detailData.mainId || '-' }}</el-descriptions-item>
<el-descriptions-item label="发票号码">{{ detailData.invoiceId || '-' }}</el-descriptions-item>
<el-descriptions-item label="发票代码">{{ detailData.invoiceCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="发票日期">{{ detailData.invoiceDate || '-' }}</el-descriptions-item>
<el-descriptions-item label="发票类型">
<el-tag size="mini" :type="Number(detailData.invoiceType) === 9 ? 'warning' : 'primary'">{{ Number(detailData.invoiceType) === 9 ? '全电发票(增值税专用发票)' : Number(detailData.invoiceType) === 10 ? '全电发票(增值税普通发票)' : invoiceTypeName(detailData.invoiceType) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="发票状态"><el-tag size="mini" :type="Number(detailData.status) === 1 ? 'success' : Number(detailData.status) === 2 ? 'danger' : 'info'">{{ Number(detailData.status) === 1 ? '成功' : Number(detailData.status) === 2 ? '失败' : '未知' }}</el-tag></el-descriptions-item>
<el-descriptions-item label="发票裸价"><span style="color: #409eff">¥{{ Number(detailData.invoiceNakeAmount || 0).toFixed(2) }}</span></el-descriptions-item>
<el-descriptions-item label="发票税率">{{ detailData.invoiceTaxRate == null ? '-' : (Number(detailData.invoiceTaxRate) * 100).toFixed(2) + '%' }}</el-descriptions-item>
<el-descriptions-item label="发票税额"><span style="color: #409eff">¥{{ Number(detailData.invoiceTaxAmount || 0).toFixed(2) }}</span></el-descriptions-item>
<el-descriptions-item label="价税合计" :span="3"><strong style="color: #f56c6c; font-size: 18px">¥{{ Number(detailData.invoiceAmount || 0).toFixed(2) }}</strong></el-descriptions-item>
<el-descriptions-item label="备注" :span="2" v-if="detailData.remark">{{ detailData.remark }}</el-descriptions-item>
<el-descriptions-item label="电子发票">
<template v-if="detailData.url || detailData.imageEncode">
<el-button @click="previewInvoice(detailData)" size="mini" type="text" icon="el-icon-view">预览PDF</el-button>
<el-link :href="detailData.url || detailData.imageEncode" :download="(detailData.invoiceId || '发票') + '.' + (detailData.fileType || 'pdf').toLowerCase()" type="success" :underline="false" icon="el-icon-download" style="margin-left: 16px">下载PDF</el-link>
</template>
<span v-else>暂无地址</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
<span slot="footer">
<el-button v-if="detailData.id && (Number(detailData.status) === 2 || !detailData.url)" @click="retryInvoice(detailData.mainId)" :loading="retryLoading" type="primary">重新获取发票</el-button>
<el-button @click="detailDialogVisible = false">关闭</el-button>
</span>
</el-dialog>
<el-dialog :close-on-click-modal="false" title="PDF预览" :visible.sync="pdfDialogVisible" append-to-body width="80%" @closed="closePdfPreview">
<div v-loading="pdfLoading" style="height: 70vh">
<iframe v-if="pdfUrl" :src="pdfUrl" title="发票PDF预览" style="width: 100%; height: 100%; border: 0"></iframe>
</div>
<span slot="footer"><el-button @click="pdfDialogVisible = false">关闭</el-button></span>
</el-dialog>
</div> </div>
<script nonce="${cspNonce!}"> <script nonce="${cspNonce!}">
@@ -236,7 +161,6 @@ layout("/layouts/platform.html"){
mixins: [initTableMixins], mixins: [initTableMixins],
data() { data() {
return { return {
supplierList: [],
tableData: [], tableData: [],
tableLoading: false, tableLoading: false,
pageForm: { pageForm: {
@@ -244,48 +168,28 @@ layout("/layouts/platform.html"){
pageSize: 10, pageSize: 10,
totalCount: 0, totalCount: 0,
settlementId: null, settlementId: null,
bSuccess: null, bSuccess: null
sucOrderIds: null,
failOrderIds: null,
failMsg: null
}, },
applyDialogVisible: false,
applyLoading: false,
applyDateRange: [],
orderTotalPoints: 0, orderTotalPoints: 0,
applyForm: this.emptyApplyForm(),
orderDialogVisible: false, orderDialogVisible: false,
orderLoading: false, orderLoading: false,
orderDateRange: [], orderDateRange: [],
orderList: [], orderList: [],
orderSelections: [],
orderForm: this.emptyOrderForm(), orderForm: this.emptyOrderForm(),
infoDialogVisible: false, infoDialogVisible: false,
infoLoading: false, infoLoading: false,
infoList: [], infoList: [],
infoForm: this.emptyInfoForm() infoForm: this.emptyInfoForm(),
detailDialogVisible: false,
detailLoading: false,
retryLoading: false,
pdfDialogVisible: false,
pdfLoading: false,
pdfUrl: null,
detailData: {}
} }
}, },
methods: { methods: {
emptyApplyForm() {
return {
supplierId: null,
orderIds: null,
startDate: null,
endDate: null,
invoiceDate: null,
invoiceTitle: null,
invoiceCode: null,
invoiceAddress: null,
invoiceContact: null,
invoiceType: 10,
invoiceContent: null,
remark: null,
bankAccount: null,
registeredAddress: null,
bankName: null
}
},
emptyOrderForm() { emptyOrderForm() {
return { return {
pageNumber: 1, pageNumber: 1,
@@ -295,7 +199,7 @@ layout("/layouts/platform.html"){
orderId: null, orderId: null,
startDate: null, startDate: null,
endDate: null, endDate: null,
invoiceStatus: 0 invoiceStatus: null
} }
}, },
emptyInfoForm() { emptyInfoForm() {
@@ -343,79 +247,21 @@ layout("/layouts/platform.html"){
this.pageForm.pageNumber = 1 this.pageForm.pageNumber = 1
this.pageData() this.pageData()
}, },
async loadSuppliers() {
const resp = await this.$axios.post(loc() + "/supplierList")
if (resp.code === 0) {
this.supplierList = resp.data || []
}
},
openApplyDialog() {
this.applyForm = this.emptyApplyForm()
this.applyDateRange = []
this.orderTotalPoints = 0
this.applyDialogVisible = true
},
syncApplyDateRange() {
this.applyForm.startDate = this.applyDateRange && this.applyDateRange.length ? this.applyDateRange[0] : null
this.applyForm.endDate = this.applyDateRange && this.applyDateRange.length ? this.applyDateRange[1] : null
},
async loadOrderTotalPoints() {
this.syncApplyDateRange()
if (!this.applyForm.supplierId || !this.applyForm.orderIds) {
this.notifyWarning("请先选择供应商并填写订单ID")
return
}
const resp = await this.$axios.post(loc() + "/orderTotalPoints", {
supplierId: this.applyForm.supplierId,
orderId: this.applyForm.orderIds,
startDate: this.applyForm.startDate,
endDate: this.applyForm.endDate,
invoiceStatus: 0
})
if (resp.code === 0) {
this.orderTotalPoints = resp.data || 0
} else {
this.notifyWarning(resp.msg)
}
},
async submitApply() {
this.syncApplyDateRange()
this.applyLoading = true
const resp = await this.$axios.post(loc() + "/applyInvoice", this.applyForm)
this.applyLoading = false
if (resp.code === 0) {
this.notifySuccess(resp.data || "开票申请成功")
this.applyDialogVisible = false
this.pageData()
} else {
this.notifyWarning(resp.msg)
}
},
openOrderDialog(orderIds) { openOrderDialog(orderIds) {
this.orderDialogVisible = true this.orderDialogVisible = true
this.orderForm = this.emptyOrderForm() this.orderForm = this.emptyOrderForm()
this.orderDateRange = [] this.orderDateRange = []
if (typeof orderIds === "string" && orderIds) { this.orderList = []
this.orderForm.orderId = orderIds this.orderTotalPoints = 0
this.orderForm.invoiceStatus = null if (typeof orderIds !== "string" || !orderIds.trim()) {
return
} }
this.orderForm.orderId = orderIds
this.orderPageData() this.orderPageData()
}, this.loadOrderTotalPoints()
syncOrderDateRange() {
this.orderForm.startDate = this.orderDateRange && this.orderDateRange.length ? this.orderDateRange[0] : null
this.orderForm.endDate = this.orderDateRange && this.orderDateRange.length ? this.orderDateRange[1] : null
},
orderSearch() {
this.orderForm.pageNumber = 1
this.orderPageData()
},
orderReset() {
this.orderForm = this.emptyOrderForm()
this.orderDateRange = []
this.orderPageData()
}, },
async orderPageData() { async orderPageData() {
this.syncOrderDateRange() if (!this.orderForm.orderId) return
this.orderLoading = true this.orderLoading = true
const resp = await this.$axios.post(loc() + "/orderPage", this.orderForm) const resp = await this.$axios.post(loc() + "/orderPage", this.orderForm)
this.orderLoading = false this.orderLoading = false
@@ -435,27 +281,77 @@ layout("/layouts/platform.html"){
this.orderForm.pageNumber = 1 this.orderForm.pageNumber = 1
this.orderPageData() this.orderPageData()
}, },
orderSelectionChange(rows) { async loadOrderTotalPoints() {
this.orderSelections = rows || [] const resp = await this.$axios.post(loc() + "/orderTotalPoints", this.orderForm)
if (resp.code === 0) this.orderTotalPoints = resp.data || 0
else this.notifyWarning(resp.msg)
}, },
useOrdersForApply() { async openInvoiceDetail(row) {
if (!this.orderSelections.length) { this.detailData = {}
this.notifyWarning("请先选择订单") this.detailDialogVisible = true
this.detailLoading = true
try {
const resp = await this.$axios.post(loc() + "/infoDetail", { id: row.id })
if (resp.code === 0 && resp.data) this.detailData = resp.data
else this.notifyWarning(resp.msg || "发票详情不存在")
} finally {
this.detailLoading = false
}
},
async retryInvoice(mainId) {
if (!mainId || this.retryLoading) return
this.retryLoading = true
try {
const resp = await this.$axios.post(loc() + "/retryInvoice", { mainId })
if (resp.code !== 0) this.notifyWarning(resp.msg || "重新获取发票失败")
else this.$message.success("发票已重新获取")
await this.pageData()
if (this.infoDialogVisible && this.infoForm.mainId === mainId) await this.infoPageData()
if (this.detailDialogVisible && this.detailData.mainId === mainId) {
await this.openInvoiceDetail({ id: this.detailData.id })
}
} catch (e) {
this.notifyWarning(e.message || "重新获取发票失败")
} finally {
this.retryLoading = false
}
},
async previewInvoice(row) {
const path = row.url || row.imageEncode
if (!path) {
this.notifyWarning("暂无可预览的发票文件")
return return
} }
const first = this.orderSelections[0] if ((row.fileType || "pdf").toLowerCase() !== "pdf") {
this.applyForm = this.emptyApplyForm() this.$commonUtil.previewFile({ suffix: row.fileType.toLowerCase(), downloadPath: path, name: row.invoiceId || "发票" })
this.applyForm.supplierId = first.supplierId return
this.applyForm.orderIds = this.orderSelections.map((item) => item.orderId).join(",") }
this.applyForm.invoiceDate = new Date().toISOString().slice(0, 10) if (this.pdfLoading) return
this.applyDateRange = [this.orderForm.startDate, this.orderForm.endDate].filter(Boolean) this.closePdfPreview()
this.orderDialogVisible = false this.pdfDialogVisible = true
this.applyDialogVisible = true this.pdfLoading = true
this.loadOrderTotalPoints() try {
const response = await fetch(path, { credentials: "same-origin" })
if (!response.ok) throw new Error("获取发票文件失败")
const bytes = await response.arrayBuffer()
const signature = String.fromCharCode.apply(null, new Uint8Array(bytes, 0, Math.min(5, bytes.byteLength)))
if (signature !== "%PDF-") throw new Error("返回内容不是有效的PDF文件")
if (this.pdfDialogVisible) this.pdfUrl = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }))
} catch (e) {
this.notifyWarning(e.message || "发票预览失败")
this.pdfDialogVisible = false
} finally {
this.pdfLoading = false
}
},
closePdfPreview() {
if (this.pdfUrl) URL.revokeObjectURL(this.pdfUrl)
this.pdfUrl = null
}, },
openInfoDialog(row) { openInfoDialog(row) {
this.infoForm = this.emptyInfoForm() this.infoForm = this.emptyInfoForm()
this.infoForm.mainId = row.id this.infoForm.mainId = row.id
this.infoList = []
this.infoDialogVisible = true this.infoDialogVisible = true
this.infoPageData() this.infoPageData()
}, },
@@ -470,6 +366,7 @@ layout("/layouts/platform.html"){
if (resp.code === 0) { if (resp.code === 0) {
this.infoList = resp.data.list || [] this.infoList = resp.data.list || []
this.infoForm.totalCount = resp.data.totalCount || 0 this.infoForm.totalCount = resp.data.totalCount || 0
this.pageData()
} else { } else {
this.notifyWarning(resp.msg) this.notifyWarning(resp.msg)
} }
@@ -479,10 +376,18 @@ layout("/layouts/platform.html"){
this.infoPageData() this.infoPageData()
}, },
invoiceResultName(value) { invoiceResultName(value) {
return Number(value) === 0 ? "成功" : "失败" return value == null ? "开票中" : Number(value) === 0 ? "成功" : "失败"
}, },
invoiceResultType(value) { invoiceResultType(value) {
return Number(value) === 0 ? "success" : "danger" return value == null ? "warning" : Number(value) === 0 ? "success" : "danger"
},
orderStateName(value) {
const map = { 0: "待付款", 1: "已支付", 2: "进行中", 3: "已取消", 4: "已签收", 5: "已完成", 6: "换货", 7: "退货", 8: "已废除" }
return map[value] || "-"
},
orderStatusType(value) {
const map = { 0: "info", 1: "primary", 2: "warning", 3: "danger", 4: "primary", 5: "success", 6: "warning", 7: "danger", 8: "info" }
return map[value] || "info"
}, },
invoiceStatusName(value) { invoiceStatusName(value) {
const map = { 0: "未开票", 1: "已开票", 2: "开票中" } const map = { 0: "未开票", 1: "已开票", 2: "开票中" }
@@ -498,8 +403,11 @@ layout("/layouts/platform.html"){
} }
}, },
created() { created() {
this.loadSuppliers()
this.pageData() this.pageData()
},
beforeDestroy() {
this.pdfDialogVisible = false
this.closePdfPreview()
} }
}) })
</script> </script>
@@ -3,44 +3,74 @@ layout("/layouts/platform.html"){
#--> #-->
<div id="app" v-cloak> <div id="app" v-cloak>
<el-card shadow="never"> <el-card :body-style="{ paddingBottom: '2px' }" shadow="never">
<search @search="doSearch"> <el-form label-width="96px" @submit.native.prevent>
<search-item label="供应商"> <el-row :gutter="8">
<el-col :lg="4" :md="12" :xs="24">
<el-form-item label="供应商">
<el-select clearable filterable placeholder="请选择供应商" style="width: 100%" v-model="pageForm.supplierId"> <el-select clearable filterable placeholder="请选择供应商" style="width: 100%" v-model="pageForm.supplierId">
<el-option :key="item.supplierId" :label="item.supplierName" :value="item.supplierId" v-for="item in supplierList"></el-option> <el-option :key="item.supplierId" :label="item.supplierName" :value="item.supplierId" v-for="item in supplierList"></el-option>
</el-select> </el-select>
</search-item> </el-form-item>
<search-item label="订单ID"> </el-col>
<el-input clearable placeholder="多个订单ID用英文逗号分隔" v-model="pageForm.orderId"></el-input> <el-col :lg="4" :md="12" :xs="24">
</search-item> <el-form-item label="起始日期">
<search-item label="用户ID">
<el-input clearable placeholder="请输入用户ID" v-model="pageForm.userId"></el-input>
</search-item>
<search-item label="起始日期">
<el-date-picker clearable placeholder="开始日期" style="width: 100%" type="date" value-format="yyyy-MM-dd" v-model="pageForm.startDate"></el-date-picker> <el-date-picker clearable placeholder="开始日期" style="width: 100%" type="date" value-format="yyyy-MM-dd" v-model="pageForm.startDate"></el-date-picker>
</search-item> </el-form-item>
<search-item label="结束日期"> </el-col>
<el-col :lg="4" :md="12" :xs="24">
<el-form-item label="结束日期">
<el-date-picker clearable placeholder="结束日期" style="width: 100%" type="date" value-format="yyyy-MM-dd" v-model="pageForm.endDate"></el-date-picker> <el-date-picker clearable placeholder="结束日期" style="width: 100%" type="date" value-format="yyyy-MM-dd" v-model="pageForm.endDate"></el-date-picker>
</search-item> </el-form-item>
<search-item label="对账状态"> </el-col>
<el-select clearable placeholder="请选择对账状态" style="width: 100%" v-model="pageForm.reconciliationStatus"> <el-col :lg="4" :md="12" :xs="24">
<el-option :key="item.value" :label="item.label" :value="item.value" v-for="item in reconciliationStatusOptions"></el-option> <el-form-item label="对账类型">
<el-select clearable placeholder="请选择对账类型" style="width: 100%" v-model="pageForm.reconciliationType">
<el-option label="正常订单对账" :value="1"></el-option>
<el-option label="历史异常订单对账" :value="2"></el-option>
</el-select> </el-select>
</search-item> </el-form-item>
<search-item label="发票状态"> </el-col>
<el-col :lg="4" :md="12" :xs="24">
<el-form-item label="发票状态">
<el-select clearable placeholder="请选择发票状态" style="width: 100%" v-model="pageForm.invoiceStatus"> <el-select clearable placeholder="请选择发票状态" style="width: 100%" v-model="pageForm.invoiceStatus">
<el-option :key="item.value" :label="item.label" :value="item.value" v-for="item in invoiceStatusOptions"></el-option> <el-option :key="item.value" :label="item.label" :value="item.value" v-for="item in invoiceStatusOptions"></el-option>
</el-select> </el-select>
</search-item> </el-form-item>
</search> </el-col>
<el-col :lg="4" :md="12" :xs="24">
<el-form-item label="对账状态">
<el-select clearable placeholder="请选择对账状态" style="width: 100%" v-model="pageForm.reconciliationStatus">
<el-option :key="item.value" :label="item.label" :value="item.value" v-for="item in reconciliationStatusOptions"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :lg="6" :md="24" :xs="24">
<el-form-item label="订单ID">
<el-input clearable placeholder="多个订单ID用英文逗号分隔" v-model="pageForm.orderId"></el-input>
</el-form-item>
</el-col>
<el-col :lg="12" :md="24" :xs="24">
<el-form-item label-width="16px">
<el-button @click="doSearch" native-type="button" type="primary">查询</el-button>
<el-button @click="resetSearch" native-type="button">重置</el-button>
<el-button @click="openReconcileDialog" native-type="button" type="success">对账</el-button>
<el-button
v-if="pageForm.reconciliationStatus === 1 && hasSearched && pageForm.invoiceStatus === 0"
@click="openApplyDialog"
native-type="button"
type="warning"
>开票申请</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card> </el-card>
<el-card class="mt10" shadow="never"> <el-card class="mt10" shadow="never">
<table-tool label="对账订单列表"> <table-tool label="对账订单列表"></table-tool>
<el-button @click="openReconcileDialog" size="mini" type="success">对账</el-button> <el-table ref="tableRef" :data="tableData" @selection-change="selectionChange" border class="vi-table" size="small" style="width: 100%" v-loading="tableLoading">
<el-button @click="openRecordDialog" size="mini" type="primary">对账记录</el-button> <el-table-column align="center" type="selection" width="50"></el-table-column>
</table-tool>
<el-table :data="tableData" border class="vi-table" size="small" style="width: 100%" v-loading="tableLoading">
<el-table-column :index="indexMethod" align="center" label="序号" type="index" width="70"></el-table-column> <el-table-column :index="indexMethod" align="center" label="序号" type="index" width="70"></el-table-column>
<el-table-column align="center" label="订单ID" min-width="170" prop="orderId" show-overflow-tooltip></el-table-column> <el-table-column align="center" label="订单ID" min-width="170" prop="orderId" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="供应商" min-width="150" prop="supplierName" show-overflow-tooltip></el-table-column> <el-table-column align="center" label="供应商" min-width="150" prop="supplierName" show-overflow-tooltip></el-table-column>
@@ -87,39 +117,46 @@ layout("/layouts/platform.html"){
</el-card> </el-card>
<el-dialog :close-on-click-modal="false" title="确认对账" :visible.sync="reconcileDialogVisible" width="480px"> <el-dialog :close-on-click-modal="false" title="确认对账" :visible.sync="reconcileDialogVisible" width="480px">
<el-form label-width="100px" size="small"> <el-alert :closable="false" show-icon style="margin-bottom: 16px" :title="reconcileSelectionCount ? '本次仅对已勾选的 ' + reconcileSelectionCount + ' 笔订单执行对账。' : '您尚未勾选订单,本次将对当前查询条件下所有符合对账条件的订单执行对账(包含其他分页)。请确认对账范围。'" :type="reconcileSelectionCount ? 'info' : 'warning'"></el-alert>
<el-form-item label="供应商"> <el-descriptions :column="1" border size="small">
<el-select clearable filterable placeholder="请选择供应商" style="width: 100%" v-model="reconcileForm.supplierId"> <el-descriptions-item label="本次对账金额">{{ reconcilePointsTotal }}</el-descriptions-item>
<el-option :key="item.supplierId" :label="item.supplierName" :value="item.supplierId" v-for="item in supplierList"></el-option> </el-descriptions>
</el-select> <el-alert :closable="false" show-icon style="margin-top: 16px" title="请记录当前对账积分金额,确认对账后该数值将不再展示。" type="warning"></el-alert>
</el-form-item>
<el-form-item label="对账类型">
<el-select placeholder="请选择对账类型" style="width: 100%" v-model="reconcileForm.reconciliationType">
<el-option label="正常订单对账" :value="1"></el-option>
<el-option label="历史异常订单对账" :value="2"></el-option>
</el-select>
</el-form-item>
<el-form-item label="起始日期">
<el-date-picker placeholder="开始日期" style="width: 100%" type="date" value-format="yyyy-MM-dd" v-model="reconcileForm.startDate"></el-date-picker>
</el-form-item>
<el-form-item label="结束日期">
<el-date-picker placeholder="结束日期" style="width: 100%" type="date" value-format="yyyy-MM-dd" v-model="reconcileForm.endDate"></el-date-picker>
</el-form-item>
<el-form-item label="订单ID">
<el-input clearable placeholder="多个订单ID用英文逗号分隔" v-model="reconcileForm.orderId"></el-input>
</el-form-item>
<el-form-item label="积分合计">
<span>{{ reconcilePointsTotal }}</span>
</el-form-item>
</el-form>
<el-alert :closable="false" show-icon title="确认后订单将更新为已对账或对账异常,并生成对账记录。" type="warning"></el-alert>
<span slot="footer"> <span slot="footer">
<el-button @click="reconcileDialogVisible = false">取消</el-button> <el-button @click="reconcileDialogVisible = false">取消</el-button>
<el-button @click="loadPointsTotal" :loading="pointsTotalLoading">计算金额</el-button>
<el-button @click="executeReconciliation" :loading="executeLoading" type="primary">确认对账</el-button> <el-button @click="executeReconciliation" :loading="executeLoading" type="primary">确认对账</el-button>
</span> </span>
</el-dialog> </el-dialog>
<el-dialog :close-on-click-modal="false" title="开票申请" :visible.sync="applyDialogVisible" top="5vh" width="720px">
<el-alert :closable="false" show-icon type="info" style="margin-bottom: 16px"
:title="applySelectionCount ? '本次仅为已勾选的 ' + applySelectionCount + ' 笔订单申请开票。' : '未勾选订单,本次将为当前查询条件下的全部可开票订单申请开票(包含其他分页)。'"></el-alert>
<el-form ref="applyFormRef" :model="applyForm" :rules="applyRules" label-width="120px" size="small">
<el-form-item label="开票日期" prop="invoiceDate">
<el-date-picker placeholder="请选择开票日期" style="width: 100%" type="date" value-format="yyyy-MM-dd" v-model="applyForm.invoiceDate"></el-date-picker>
</el-form-item>
<el-form-item label="发票抬头" prop="invoiceTitle" required>
<el-input clearable placeholder="请输入发票抬头" v-model="applyForm.invoiceTitle"></el-input>
</el-form-item>
<el-form-item label="纳税人识别号" prop="taxIdNumber" required>
<el-input clearable placeholder="请输入纳税人识别号" v-model="applyForm.taxIdNumber"></el-input>
</el-form-item>
<el-form-item label="开户地址" prop="invoiceAddress" required>
<el-input clearable placeholder="请输入发票地址" v-model="applyForm.invoiceAddress"></el-input>
</el-form-item>
<el-form-item label="联系电话" prop="invoiceContact" required>
<el-input clearable maxlength="11" placeholder="请输入联系电话" v-model="applyForm.invoiceContact"></el-input>
</el-form-item>
<el-form-item label="发票类型" prop="invoiceType" required>
<dict-select clearable code="invoice_type" placeholder="请选择发票类型" style="width: 100%" v-model="applyForm.invoiceType"></dict-select>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="applyDialogVisible = false">取消</el-button>
<el-button @click="submitApply" :loading="applyLoading" type="primary">确认开票</el-button>
</span>
</el-dialog>
<el-dialog :close-on-click-modal="false" title="对账记录" :visible.sync="recordDialogVisible" top="5vh" width="90%"> <el-dialog :close-on-click-modal="false" title="对账记录" :visible.sync="recordDialogVisible" top="5vh" width="90%">
<el-form :inline="true" size="small" @submit.native.prevent> <el-form :inline="true" size="small" @submit.native.prevent>
<el-form-item label="供应商"> <el-form-item label="供应商">
@@ -209,6 +246,10 @@ layout("/layouts/platform.html"){
supplierList: [], supplierList: [],
tableData: [], tableData: [],
tableLoading: false, tableLoading: false,
selections: [],
lastQuery: null,
reconcileSelectionCount: 0,
hasSearched: false,
pageForm: { pageForm: {
pageNumber: 1, pageNumber: 1,
pageSize: 10, pageSize: 10,
@@ -218,6 +259,7 @@ layout("/layouts/platform.html"){
userId: null, userId: null,
startDate: null, startDate: null,
endDate: null, endDate: null,
reconciliationType: null,
reconciliationStatus: null, reconciliationStatus: null,
invoiceStatus: null invoiceStatus: null
}, },
@@ -232,6 +274,38 @@ layout("/layouts/platform.html"){
startDate: null, startDate: null,
endDate: null endDate: null
}, },
applyDialogVisible: false,
applyLoading: false,
applyScope: {},
applySelectionCount: 0,
applyForm: {
invoiceDate: [new Date().getFullYear(), String(new Date().getMonth() + 1).padStart(2, "0"), String(new Date().getDate()).padStart(2, "0")].join("-"),
invoiceTitle: "测试工会(开发测试)",
taxIdNumber: "91320100MA00000000",
invoiceAddress: "江苏省南京市测试路1号(开发测试)",
invoiceContact: "13800000000",
invoiceType: 10
},
applyRules: {
invoiceDate: [{ required: true, message: "请选择开票日期", trigger: "change" }],
invoiceTitle: [{ required: true, message: "请输入发票抬头", trigger: "blur" }],
taxIdNumber: [{ required: true, message: "请输入纳税人识别号", trigger: "blur" }],
invoiceAddress: [{ required: true, message: "请输入开户地址", trigger: "blur" }],
invoiceContact: [
{ required: true, message: "请输入联系电话", trigger: "blur" },
{
validator(rule, value, callback) {
if (value && !/^1[3-9]\d{9}$/.test(value)) {
callback(new Error("请输入正确的手机号码"))
} else {
callback()
}
},
trigger: "blur"
}
],
invoiceType: [{ required: true, message: "请选择发票类型", trigger: "change" }]
},
recordDialogVisible: false, recordDialogVisible: false,
recordLoading: false, recordLoading: false,
recordList: [], recordList: [],
@@ -279,14 +353,41 @@ layout("/layouts/platform.html"){
return index + 1 + (this.recordForm.pageNumber - 1) * this.recordForm.pageSize return index + 1 + (this.recordForm.pageNumber - 1) * this.recordForm.pageSize
}, },
doSearch() { doSearch() {
this.hasSearched = true
this.selections = []
this.pageForm.pageNumber = 1 this.pageForm.pageNumber = 1
this.pageData() this.pageData()
}, },
resetSearch() {
this.pageForm = {
pageNumber: 1,
pageSize: 10,
totalCount: 0,
supplierId: null,
orderId: null,
userId: null,
startDate: null,
endDate: null,
reconciliationType: null,
reconciliationStatus: null,
invoiceStatus: null
}
this.hasSearched = false
this.selections = []
this.pageData()
},
selectionChange(rows) {
this.selections = rows || []
},
async pageData() { async pageData() {
this.tableLoading = true this.tableLoading = true
const resp = await this.$axios.post(loc() + "/pageData", this.pageForm) const query = Object.assign({}, this.pageForm)
const resp = await this.$axios.post(loc() + "/pageData", query)
this.tableLoading = false this.tableLoading = false
if (resp.code === 0) { if (resp.code === 0) {
this.lastQuery = query
this.selections = []
if (this.$refs.tableRef) this.$refs.tableRef.clearSelection()
this.tableData = resp.data.list || [] this.tableData = resp.data.list || []
this.pageForm.totalCount = resp.data.totalCount || 0 this.pageForm.totalCount = resp.data.totalCount || 0
} else { } else {
@@ -308,40 +409,67 @@ layout("/layouts/platform.html"){
this.supplierList = resp.data || [] this.supplierList = resp.data || []
} }
}, },
openReconcileDialog() { async openReconcileDialog() {
this.reconcileForm = { if (this.tableLoading || !this.lastQuery) {
supplierId: this.pageForm.supplierId, this.notifyWarning("请先查询订单列表")
reconciliationType: this.pageForm.reconciliationStatus === 2 ? 2 : 1,
orderId: this.pageForm.orderId,
startDate: this.pageForm.startDate,
endDate: this.pageForm.endDate
}
this.reconcilePointsTotal = 0
this.reconcileDialogVisible = true
this.loadPointsTotal()
},
async loadPointsTotal() {
if (!this.reconcileForm.supplierId || !this.reconcileForm.startDate || !this.reconcileForm.endDate || !this.reconcileForm.reconciliationType) {
this.notifyWarning("请先选择供应商、对账类型和对账日期")
return return
} }
const query = this.lastQuery
this.reconcileSelectionCount = this.selections.length
this.reconcileForm = {
supplierId: query.supplierId,
reconciliationType: query.reconciliationType,
orderId: this.selections.map(item => item.orderId).join(",") || null,
queryOrderId: query.orderId,
userId: query.userId,
reconciliationStatus: query.reconciliationStatus,
invoiceStatus: query.invoiceStatus,
startDate: query.startDate,
endDate: query.endDate
}
this.reconcilePointsTotal = 0
if (!this.reconciliationParamsComplete()) {
this.notifyWarning("请选择供应商、对账类型和对账日期,并查询订单列表")
return
}
if (await this.loadPointsTotal()) {
this.reconcileDialogVisible = true
}
},
reconciliationParamsComplete() {
if (!this.reconcileForm.supplierId || !this.reconcileForm.reconciliationType) {
return false
}
if (!this.reconcileForm.startDate || !this.reconcileForm.endDate) {
return false
}
return true
},
async loadPointsTotal() {
if (!this.reconciliationParamsComplete()) {
return false
}
this.pointsTotalLoading = true this.pointsTotalLoading = true
try {
const resp = await this.$axios.post(loc() + "/pointsTotal", this.reconcileForm) const resp = await this.$axios.post(loc() + "/pointsTotal", this.reconcileForm)
this.pointsTotalLoading = false
if (resp.code === 0) { if (resp.code === 0) {
this.reconcilePointsTotal = resp.data || 0 this.reconcilePointsTotal = resp.data || 0
} else { return true
}
this.notifyWarning(resp.msg) this.notifyWarning(resp.msg)
return false
} finally {
this.pointsTotalLoading = false
} }
}, },
async executeReconciliation() { async executeReconciliation() {
if (!this.reconcileForm.supplierId || !this.reconcileForm.startDate || !this.reconcileForm.endDate || !this.reconcileForm.reconciliationType) { if (!this.reconciliationParamsComplete()) {
this.notifyWarning("请先选择供应商、对账类型和对账日期") this.notifyWarning("请先选择供应商、对账类型和对账日期,并查询订单列表")
return return
} }
this.executeLoading = true this.executeLoading = true
try {
const resp = await this.$axios.post(loc() + "/execute", this.reconcileForm) const resp = await this.$axios.post(loc() + "/execute", this.reconcileForm)
this.executeLoading = false
if (resp.code === 0) { if (resp.code === 0) {
this.notifySuccess(resp.data || "对账完成") this.notifySuccess(resp.data || "对账完成")
this.reconcileDialogVisible = false this.reconcileDialogVisible = false
@@ -349,11 +477,66 @@ layout("/layouts/platform.html"){
} else { } else {
this.notifyWarning(resp.msg) this.notifyWarning(resp.msg)
} }
} finally {
this.executeLoading = false
}
},
openApplyDialog() {
if (this.tableLoading || !this.lastQuery) {
this.notifyWarning("请先查询订单列表")
return
}
const query = this.lastQuery
if (!query.supplierId || !query.startDate || !query.endDate || query.reconciliationStatus !== 1 || query.invoiceStatus !== 0) {
this.notifyWarning("请查询对账成功且未开票的订单")
return
}
this.applySelectionCount = this.selections.length
this.applyScope = {
supplierId: query.supplierId,
startDate: query.startDate,
endDate: query.endDate,
queryOrderId: query.orderId,
userId: query.userId,
orderIds: this.selections.map(item => item.orderId).join(",") || null
}
this.applyForm = {
invoiceDate: [new Date().getFullYear(), String(new Date().getMonth() + 1).padStart(2, "0"), String(new Date().getDate()).padStart(2, "0")].join("-"),
invoiceTitle: "测试工会(开发测试)",
taxIdNumber: "91320100MA00000000",
invoiceAddress: "江苏省南京市测试路1号(开发测试)",
invoiceContact: "13800000000",
invoiceType: 10
}
this.applyDialogVisible = true
},
submitApply() {
this.$refs.applyFormRef.validate(async (valid) => {
if (!valid || this.applyLoading) {
return
}
this.applyLoading = true
try {
const params = Object.assign({}, this.applyForm, this.applyScope)
const resp = await this.$axios.post("/platform/zhgh/points-mall/invoice/applyInvoice", params)
if (resp.code === 0) {
this.notifySuccess(resp.data || "开票申请成功")
this.applyDialogVisible = false
this.selections = []
this.$refs.tableRef.clearSelection()
this.pageData()
} else {
this.notifyWarning(resp.msg)
}
} finally {
this.applyLoading = false
}
})
}, },
openRecordDialog() { openRecordDialog() {
this.recordDialogVisible = true this.recordDialogVisible = true
this.recordForm.supplierId = this.pageForm.supplierId this.recordForm.supplierId = this.pageForm.supplierId
this.recordForm.orderId = this.pageForm.orderId this.recordForm.orderId = null
this.recordPageData() this.recordPageData()
}, },
recordSearch() { recordSearch() {
@@ -13,25 +13,6 @@ layout("/layouts/platform.html"){
<search-item label="订单ID"> <search-item label="订单ID">
<el-input clearable placeholder="请输入订单ID" v-model="pageForm.orderId"></el-input> <el-input clearable placeholder="请输入订单ID" v-model="pageForm.orderId"></el-input>
</search-item> </search-item>
<search-item label="用户ID">
<el-input clearable placeholder="请输入用户ID" v-model="pageForm.userId"></el-input>
</search-item>
<search-item label="批次号">
<el-input clearable placeholder="请输入对账批次号" v-model="pageForm.batchNo"></el-input>
</search-item>
<search-item label="差异类型">
<el-select clearable placeholder="请选择差异类型" style="width: 100%" v-model="pageForm.diffType">
<el-option label="一致" :value="0"></el-option>
<el-option label="异常" :value="1"></el-option>
</el-select>
</search-item>
<search-item label="通知状态">
<el-select clearable placeholder="请选择通知状态" style="width: 100%" v-model="pageForm.noticeStatus">
<el-option label="未推送" :value="0"></el-option>
<el-option label="推送成功" :value="1"></el-option>
<el-option label="推送失败" :value="2"></el-option>
</el-select>
</search-item>
</search> </search>
</el-card> </el-card>
@@ -39,11 +20,10 @@ layout("/layouts/platform.html"){
<table-tool label="对账记录"></table-tool> <table-tool label="对账记录"></table-tool>
<el-table :data="tableData" border class="vi-table" size="small" style="width: 100%" v-loading="tableLoading"> <el-table :data="tableData" border class="vi-table" size="small" style="width: 100%" v-loading="tableLoading">
<el-table-column :index="indexMethod" align="center" label="序号" type="index" width="70"></el-table-column> <el-table-column :index="indexMethod" align="center" label="序号" type="index" width="70"></el-table-column>
<el-table-column align="center" label="批次号" min-width="170" prop="batchNo" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="订单ID" min-width="170" prop="orderId" show-overflow-tooltip></el-table-column> <el-table-column align="center" label="订单ID" min-width="170" prop="orderId" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="供应商" min-width="150" prop="supplierName" show-overflow-tooltip></el-table-column> <el-table-column align="center" label="供应商" min-width="150" prop="supplierName" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="供应商编码" min-width="130" prop="supplierId" show-overflow-tooltip></el-table-column> <el-table-column align="center" label="供应商编码" min-width="130" prop="supplierId" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="用户ID" min-width="120" prop="userId" show-overflow-tooltip></el-table-column> <el-table-column align="center" label="用户工号" min-width="120" prop="loginname" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="订单总金额(平台)" min-width="140" prop="pTotalPrice"></el-table-column> <el-table-column align="center" label="订单总金额(平台)" min-width="140" prop="pTotalPrice"></el-table-column>
<el-table-column align="center" label="现金金额(平台)" min-width="130" prop="pCashAmount"></el-table-column> <el-table-column align="center" label="现金金额(平台)" min-width="130" prop="pCashAmount"></el-table-column>
<el-table-column align="center" label="积分金额(平台)" min-width="130" prop="pPointsPrice"></el-table-column> <el-table-column align="center" label="积分金额(平台)" min-width="130" prop="pPointsPrice"></el-table-column>
@@ -57,18 +37,7 @@ layout("/layouts/platform.html"){
<el-tag :type="diffTypeTag(row.diffType)" size="mini">{{ diffTypeName(row.diffType) }}</el-tag> <el-tag :type="diffTypeTag(row.diffType)" size="mini">{{ diffTypeName(row.diffType) }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column align="center" label="通知状态" min-width="110">
<template slot-scope="{row}">
<el-tag :type="noticeStatusTag(row.noticeStatus)" size="mini">{{ noticeStatusName(row.noticeStatus) }}</el-tag>
</template>
</el-table-column>
<el-table-column align="center" label="推送次数" min-width="90" prop="pushTime"></el-table-column>
<el-table-column align="center" label="差异原因" min-width="240" prop="diffReason" show-overflow-tooltip></el-table-column> <el-table-column align="center" label="差异原因" min-width="240" prop="diffReason" show-overflow-tooltip></el-table-column>
<el-table-column align="center" fixed="right" label="操作" width="90">
<template slot-scope="{row}">
<el-button @click="openDetail(row)" size="mini" type="primary">查看</el-button>
</template>
</el-table-column>
</el-table> </el-table>
<el-pagination <el-pagination
:current-page="pageForm.pageNumber" :current-page="pageForm.pageNumber"
@@ -87,7 +56,7 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="批次号">{{ currentRecord.batchNo || "-" }}</el-descriptions-item> <el-descriptions-item label="批次号">{{ currentRecord.batchNo || "-" }}</el-descriptions-item>
<el-descriptions-item label="订单ID">{{ currentRecord.orderId || "-" }}</el-descriptions-item> <el-descriptions-item label="订单ID">{{ currentRecord.orderId || "-" }}</el-descriptions-item>
<el-descriptions-item label="供应商">{{ currentRecord.supplierName || currentRecord.supplierId || "-" }}</el-descriptions-item> <el-descriptions-item label="供应商">{{ currentRecord.supplierName || currentRecord.supplierId || "-" }}</el-descriptions-item>
<el-descriptions-item label="用户ID">{{ currentRecord.userId || "-" }}</el-descriptions-item> <el-descriptions-item label="用户工号">{{ currentRecord.loginname || "-" }}</el-descriptions-item>
<el-descriptions-item label="平台总金额">{{ currentRecord.pTotalPrice || "0" }}</el-descriptions-item> <el-descriptions-item label="平台总金额">{{ currentRecord.pTotalPrice || "0" }}</el-descriptions-item>
<el-descriptions-item label="对账总金额">{{ currentRecord.sTotalPrice || "0" }}</el-descriptions-item> <el-descriptions-item label="对账总金额">{{ currentRecord.sTotalPrice || "0" }}</el-descriptions-item>
<el-descriptions-item label="平台现金金额">{{ currentRecord.pCashAmount || "0" }}</el-descriptions-item> <el-descriptions-item label="平台现金金额">{{ currentRecord.pCashAmount || "0" }}</el-descriptions-item>
@@ -121,11 +90,7 @@ layout("/layouts/platform.html"){
pageSize: 10, pageSize: 10,
totalCount: 0, totalCount: 0,
supplierId: null, supplierId: null,
orderId: null, orderId: null
userId: null,
diffType: null,
batchNo: null,
noticeStatus: null
}, },
detailDialogVisible: false, detailDialogVisible: false,
currentRecord: null currentRecord: null
@@ -14,11 +14,12 @@ const welfareOption = {
<el-table-column align="center" header-align="center" label="系统默认选择" width="140"> <el-table-column align="center" header-align="center" label="系统默认选择" width="140">
<template slot-scope="{row}"> <template slot-scope="{row}">
<el-checkbox <el-radio
:value="!!row.isSystemDefault" v-model="systemDefaultOption"
@change="setSystemDefault(row, $event)"> :label="row"
@change="setSystemDefault(row)">
&nbsp; &nbsp;
</el-checkbox> </el-radio>
</template> </template>
</el-table-column> </el-table-column>
@@ -123,10 +124,9 @@ const welfareOption = {
}, },
// 每个福利项目只允许配置一个系统默认选项,保存时会随福利选项一并提交。 // 每个福利项目只允许配置一个系统默认选项,保存时会随福利选项一并提交。
setSystemDefault(defaultOption, checked = true) { setSystemDefault(defaultOption) {
this.systemDefaultOption = checked ? defaultOption : null;
this.welfareList.forEach((item) => { this.welfareList.forEach((item) => {
this.$set(item, 'isSystemDefault', checked && item === defaultOption); this.$set(item, 'isSystemDefault', item === defaultOption);
}); });
}, },
@@ -200,7 +200,7 @@ const welfareOption = {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
/deep/ .welfare-option .el-checkbox__label { /deep/ .welfare-option .el-radio__label {
padding-left: 0; padding-left: 0;
} }
@@ -397,19 +397,6 @@ layout("/layouts/platform_h5.html"){
justify-content: flex-end; justify-content: flex-end;
} }
.sports-event-union-action-hint {
display: inline-flex;
align-items: center;
color: #ed6a0c;
font-size: 12px;
line-height: 18px;
text-align: right;
}
.sports-event-union-action-hint.is-authorized {
color: #1989fa;
}
.sports-event-page .table-card .table-card-footer .van-cell__value { .sports-event-page .table-card .table-card-footer .van-cell__value {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -713,12 +700,6 @@ layout("/layouts/platform_h5.html"){
取消报名 取消报名
</van-button> </van-button>
</div> </div>
<span
v-if="row.applyWay.includes(2) && !row.applyWay.includes(1)"
class="sports-event-union-action-hint"
:class="{'is-authorized': canUnionApplyOnPc}">
{{ canUnionApplyOnPc ? '分工会报名请在PC端进行' : '该活动为分工会报名,暂无权限' }}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -750,12 +731,6 @@ layout("/layouts/platform_h5.html"){
<i class="fa fa-trash"></i>取消报名 <i class="fa fa-trash"></i>取消报名
</van-button> </van-button>
</div> </div>
<span
v-if="row.applyWay.includes(2) && !row.applyWay.includes(1)"
class="sports-event-union-action-hint"
:class="{'is-authorized': canUnionApplyOnPc}">
{{ canUnionApplyOnPc ? '分工会报名请在PC端进行' : '该活动为分工会报名,暂无权限' }}
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -793,16 +768,6 @@ layout("/layouts/platform_h5.html"){
} }
}, },
components: {}, components: {},
computed: {
canUnionApplyOnPc() {
return this.$auth.hasRoleOr([
"SYSADMIN",
"SCHOOL_UNION_ADMIN",
"BRANCH_UNION_CHAIRMAN",
"BRANCH_UNION_WENTI_SPORTS"
]) && this.$auth.hasPermission("activity.apply")
}
},
methods: { methods: {
toggleViewMode() { toggleViewMode() {
this.viewMode = this.viewMode === "card" ? "list" : "card" this.viewMode = this.viewMode === "card" ? "list" : "card"
@@ -324,37 +324,11 @@ layout("/layouts/platform_h5.html"){
} }
.brand-course-card__detail:nth-child(-n + 2) .brand-course-card__detail-label, .brand-course-card__detail:nth-child(-n + 2) .brand-course-card__detail-label,
.brand-course-card__detail:nth-child(2) .brand-course-card__detail-value { .brand-course-card__detail:nth-child(-n + 2) .brand-course-card__detail-value {
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: normal; overflow-wrap: normal;
white-space: nowrap; white-space: nowrap;
} }
.brand-course-card__detail:first-child .brand-course-card__detail-value {
display: block;
max-width: 100%;
overflow: hidden;
overflow-wrap: normal;
text-overflow: ellipsis;
white-space: nowrap;
}
.brand-course-contact-popover {
display: block;
min-width: 0;
max-width: 100%;
}
.brand-course-contact-popover__content {
max-width: 240px;
padding: 8px 12px;
overflow-wrap: anywhere;
color: #33445a;
font-size: 12px;
line-height: 18px;
}
.brand-course-card__links { .brand-course-card__links {
display: flex; display: flex;
min-width: 0; min-width: 0;
@@ -939,17 +913,7 @@ layout("/layouts/platform_h5.html"){
<van-icon name="friends-o"></van-icon> <van-icon name="friends-o"></van-icon>
<div class="brand-course-card__detail-copy"> <div class="brand-course-card__detail-copy">
<div class="brand-course-card__detail-label">联系人</div> <div class="brand-course-card__detail-label">联系人</div>
<van-popover <div class="brand-course-card__detail-value">{{row.courseInstructor}}</div>
class="brand-course-contact-popover"
:value="contactPopoverIndex === index"
placement="top-start"
trigger="click"
@input="setContactPopover(index, $event)">
<div class="brand-course-contact-popover__content">{{row.courseInstructor || '暂无'}}</div>
<template #reference>
<div class="brand-course-card__detail-value">{{row.courseInstructor || '暂无'}}</div>
</template>
</van-popover>
</div> </div>
</div> </div>
<div class="brand-course-card__detail"> <div class="brand-course-card__detail">
@@ -1045,7 +1009,6 @@ layout("/layouts/platform_h5.html"){
activeAssort: null, activeAssort: null,
filterOpened: false, filterOpened: false,
viewMode: 'card', viewMode: 'card',
contactPopoverIndex: -1,
weekdayCNMap: {0: '周日', 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六'}, weekdayCNMap: {0: '周日', 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六'},
} }
@@ -1054,9 +1017,6 @@ layout("/layouts/platform_h5.html"){
toggleViewMode() { toggleViewMode() {
this.viewMode = this.viewMode === 'card' ? 'list' : 'card' this.viewMode = this.viewMode === 'card' ? 'list' : 'card'
}, },
setContactPopover(index, visible) {
this.contactPopoverIndex = visible ? index : -1
},
onAssortChange(name) { onAssortChange(name) {
this.pageForm.assortTypes = name ? JSON.stringify([name]) : [] this.pageForm.assortTypes = name ? JSON.stringify([name]) : []
this.doSearch() this.doSearch()
@@ -444,7 +444,7 @@ layout("/layouts/platform_h5.html"){
<button type="button" class="works-mine-card__action" @click="onView(row)"> <button type="button" class="works-mine-card__action" @click="onView(row)">
<van-icon name="eye-o"></van-icon>查看 <van-icon name="eye-o"></van-icon>查看
</button> </button>
<button type="button" class="works-mine-card__action" @click="onEdit(row)"> <button type="button" class="works-mine-card__action is-primary" @click="onEdit(row)">
<van-icon name="edit"></van-icon>编辑 <van-icon name="edit"></van-icon>编辑
</button> </button>
<button type="button" class="works-mine-card__action is-danger" @click="onDelete(row)"> <button type="button" class="works-mine-card__action is-danger" @click="onDelete(row)">
@@ -467,7 +467,7 @@ layout("/layouts/platform_h5.html"){
:close-on-click-overlay="false" show-cancel-button cancel-button-text="取消" confirm-button-text="确认删除" :close-on-click-overlay="false" show-cancel-button cancel-button-text="取消" confirm-button-text="确认删除"
@cancel="closeDeleteConfirm" @confirm="confirmDelete"></van-dialog> @cancel="closeDeleteConfirm" @confirm="confirmDelete"></van-dialog>
<apply-form ref="applyFormRef" @submit-success="handleEditSubmitSuccess"></apply-form> <apply-form ref="applyFormRef"></apply-form>
<info ref="infoRef"></info> <info ref="infoRef"></info>
</div> </div>
@@ -541,10 +541,6 @@ layout("/layouts/platform_h5.html"){
} }
this.$refs.applyFormRef.onOpenEdit(row) this.$refs.applyFormRef.onOpenEdit(row)
}, },
// 编辑提交成功后留在我的作品页面,仅刷新当前列表数据。
handleEditSubmitSuccess() {
this.loadWorks(true)
},
onDelete(row) { onDelete(row) {
this.$set(this, 'pendingDeleteRow', row) this.$set(this, 'pendingDeleteRow', row)
window.h5HistoryLayerManager.open('works-collection-mine-delete-confirm') window.h5HistoryLayerManager.open('works-collection-mine-delete-confirm')
@@ -8,7 +8,7 @@ const INFO = {
<van-sticky> <van-sticky>
<van-nav-bar title="作品详情" left-text="返回" left-arrow @click-left="closeInfo"></van-nav-bar> <van-nav-bar title="作品详情" left-text="返回" left-arrow @click-left="closeInfo"></van-nav-bar>
</van-sticky> </van-sticky>
<div ref="detailScrollRef" class="works-detail-scroll"> <div class="works-detail-scroll">
<section class="works-detail-card"> <section class="works-detail-card">
<h2 class="works-detail-card__title">基础信息</h2> <h2 class="works-detail-card__title">基础信息</h2>
<van-cell-group :border="false"> <van-cell-group :border="false">
@@ -154,7 +154,6 @@ const INFO = {
onOpen(row) { onOpen(row) {
this.$set(this, "viewData", Object.assign({}, row, {files: this.normalizeFiles(row.files)})) this.$set(this, "viewData", Object.assign({}, row, {files: this.normalizeFiles(row.files)}))
window.h5HistoryLayerManager.open("works-collection-mine-info") window.h5HistoryLayerManager.open("works-collection-mine-info")
this.resetScrollPosition()
this.$axios.post("/platform/activity/worksCollection/common/findOne", {id: row.id}).then((res) => { this.$axios.post("/platform/activity/worksCollection/common/findOne", {id: row.id}).then((res) => {
if (res.code === 0) { if (res.code === 0) {
const viewData = res.data || {} const viewData = res.data || {}
@@ -163,12 +162,6 @@ const INFO = {
} }
}) })
}, },
// 弹窗组件会被复用,每次打开后需清除上一次查看时保留的滚动位置。
resetScrollPosition() {
this.$nextTick(() => {
if (this.$refs.detailScrollRef) this.$refs.detailScrollRef.scrollTop = 0
})
},
normalizeFiles(files) { normalizeFiles(files) {
if (Array.isArray(files)) return files if (Array.isArray(files)) return files
if (!files) return [] if (!files) return []

Some files were not shown because too many files have changed in this diff Show More