Author SHA1 Message Date
c-chengc1 45410cb0a3 Merge remote-tracking branch 'origin/feature_工会报销UI优化' into feature_工会报销UI优化 2026-09-04 16:16:09 +08:00
c-hongqw1 6ef45e5403 fix:1、提案查看的流程节点,增加排序
2、场地预约关闭按钮无法关闭的bug修复
2026-09-04 16:15:55 +08:00
c-hongqw1 4235deede7 地大bug整改 2026-09-04 16:07:12 +08:00
c-chengc1 43ae24dceb Merge remote-tracking branch 'origin/feature_程诚_0831问题修复' into release_20260831 2026-09-02 16:27:12 +08:00
c-chengc1 cccad1a852 fix:职代会pc端修复投票纪录无法正常显示工会、代表团 2026-09-02 16:26:38 +08:00
c-chengc1 0b3fef4d71 Merge remote-tracking branch 'origin/feature_工会报销UI优化' into release_20260831 2026-09-02 11:43:11 +08:00
c-hongqw1 0b44f36c83 手机端社团、工会报销bug整改 2026-09-02 10:39:56 +08:00
c-chengc1 2de6820c1a Merge remote-tracking branch 'origin/feature_周何峰' into release_20260831 2026-09-02 09:20:52 +08:00
c-zhouhf1 478b70c2d1 整改微信小程序登录
整改测试提案UI
整改测试作品征集UI
2026-09-01 18:00:36 +08:00
c-chengc1 d305317458 Merge branch 'feature_程诚_0831问题修复' into release_20260831 2026-09-01 16:00:47 +08:00
c-chengc1 982065c424 feat:职工福利PC端系统默认选择改为复选框组件;职代会PC端投票统计结果显示不正确问题修复;体育活动h5分工会报名权限提示;品牌活动h5联系人字段超长样式适配修复; 2026-09-01 16:00:11 +08:00
c-chengc1 a92f75730d feat:职工福利ui优化,我的福利支持修改福利 2026-09-01 15:04:19 +08:00
c-zhouhf1 8a0714a8e3 Merge branch 'release_20260831' into feature_周何峰 2026-08-31 15:20:19 +08:00
c-zhouhf1 bf40155fb9 品牌活动报名统计增加校工会活动管理员看全部活动
不是超级管理员加不了角色
2026-08-31 15:18:29 +08:00
105 changed files with 3360 additions and 3285 deletions
@@ -23,6 +23,7 @@ import com.budwk.app.flow.entity.ProcessCcInstance;
import com.budwk.app.flow.entity.ProcessDefine;
import com.budwk.app.flow.entity.ProcessInstance;
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.ProcessSubmitTypeEnum;
import com.budwk.app.flow.enums.ProcessTaskStateEnum;
@@ -556,11 +557,18 @@ public class ProcessInstanceServiceImpl extends BaseServiceImpl<ProcessInstance>
public void deleteProcessInstanceById(Long processInstanceId) {
List<ProcessTask> taskList = dao().query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", processInstanceId));
List<Long> taskIds = taskList.stream().map(ProcessTask::getId).toList();
dao().clear(ProcessTask.class, Cnd.where(ProcessTask::getId, "in", taskIds));
// 任务参与人依赖流程任务,删除流程时先清理参与人,避免遗留无法回查所属流程的孤立记录。
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(ProcessCcInstance.class, Cnd.where(ProcessCcInstance::getProcessInstanceId, "=", processInstanceId));
dao().clear(ProcessInstance.class, Cnd.where(ProcessInstance::getId, "=", processInstanceId));
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void deleteProcessInstanceByBusinessKey(String businessKey) {
ProcessInstance processInstance = fetch(Cnd.where(ProcessInstance::getBusinessNo, "=", businessKey));
if (processInstance != null) {
@@ -411,15 +411,6 @@ public class Sys_user extends BaseModel implements Serializable {
@ColDefine(type = ColType.VARCHAR, width = 50)
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,8 +25,6 @@ public interface SysFileService extends BaseService<Sys_file> {
*/
String uploadReturnUrl(String engine, TempFile file);
String uploadReturnUrl(String engine, String fileName, byte[] bytes);
/**
* 分页
*/
@@ -69,11 +69,6 @@ public class SysFileServiceImpl extends BaseServiceImpl<Sys_file> implements Sys
return this.storageFile(engine, file, false);
}
@Override
public String uploadReturnUrl(String engine, String fileName, byte[] bytes) {
return this.storageFile(engine, fileName, bytes, false);
}
@Override
public Pagination page(Sys_file file) {
return null;
@@ -15,6 +15,10 @@ import java.lang.reflect.Method;
public class SaCheckLoginInterceptor implements MethodInterceptor {
@Override
public void filter(InterceptorChain chain) throws Throwable {
if (SaTokenAuthIgnoreUtil.isIgnore()) {
chain.doChain();
return;
}
Method method = chain.getCallingMethod();
SaCheckLogin at = method.getAnnotation(SaCheckLogin.class);
SaManager.getStpLogic(at.type()).checkByAnnotation(at);
@@ -15,6 +15,10 @@ import java.lang.reflect.Method;
public class SaCheckPermissionInterceptor implements MethodInterceptor {
@Override
public void filter(InterceptorChain chain) throws Throwable {
if (SaTokenAuthIgnoreUtil.isIgnore()) {
chain.doChain();
return;
}
Method method = chain.getCallingMethod();
SaCheckPermission at = method.getAnnotation(SaCheckPermission.class);
SaManager.getStpLogic(at.type()).checkByAnnotation(at);
@@ -15,6 +15,10 @@ import java.lang.reflect.Method;
public class SaCheckRoleInterceptor implements MethodInterceptor {
@Override
public void filter(InterceptorChain chain) throws Throwable {
if (SaTokenAuthIgnoreUtil.isIgnore()) {
chain.doChain();
return;
}
Method method = chain.getCallingMethod();
SaCheckRole at = method.getAnnotation(SaCheckRole.class);
SaManager.getStpLogic(at.type()).checkByAnnotation(at);
@@ -0,0 +1,60 @@
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());
}
}
@@ -162,27 +162,10 @@ public class ActivityWorksCollectionManageController {
@SaCheckPermission(value = {"activity.workscollection.manage", "activity.workscollection.new"}, mode = SaMode.OR)
@Aop(TransAop.READ_COMMITTED)
public Result update(@Param("data") @Valid Activity_works_collection worksCollection) {
String validateMessage = activityWorksCollectionService.validateReferencedTypesBeforeUpdate(worksCollection);
String validateMessage = activityWorksCollectionService.updatePreservingTypeIds(worksCollection);
if (StrUtil.isNotBlank(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);
return Result.success();
}
@@ -35,6 +35,14 @@ public interface ActivityWorksCollectionService extends BaseService<Activity_wor
*/
void cancelTemplate(String id);
/**
* 更新作品征集活动及其主题、作品类型,并保留所有原有类型ID。
*
* @param worksCollection 前端提交的活动及其主题、作品类型配置
* @return 更新成功返回 {@code null};数据层级异常或删除了投稿引用类型时返回提示信息
*/
String updatePreservingTypeIds(Activity_works_collection worksCollection);
/**
* 校验编辑活动时删除的主题类型、作品类型是否已被投稿引用。
*
@@ -21,7 +21,9 @@ import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -116,6 +118,136 @@ public class ActivityWorksCollectionServiceImpl extends BaseServiceImpl<Activity
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
public String validateReferencedTypesBeforeUpdate(Activity_works_collection worksCollection) {
if (worksCollection == null || StrUtil.isBlank(worksCollection.getId())) {
@@ -218,6 +218,10 @@ public class ClubUserJoinApplyController {
}
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())) {
return Result.error(99, "您有该社团的申请记录尚未完成,请到我的申请里查看!");
}
@@ -11,6 +11,7 @@ import com.budwk.app.base.result.Result;
import com.budwk.app.base.utils.PageUtil;
import com.budwk.app.flow.constant.FlowConst;
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.ProcessTask;
import com.budwk.app.flow.enums.ProcessInstanceStateEnum;
@@ -95,6 +96,7 @@ public class ClubRefreshReportController {
sys_club_refresh info
LEFT JOIN sys_club club ON club.id = info.clubId
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_actor ta ON ta.processTaskId = t.id
$condition
@@ -176,10 +178,23 @@ public class ClubRefreshReportController {
@ApiOperation("删除")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("club.infoManage.refreshReport")
@SLog(tag = "社团管理系统-信息管理", msg = "删除换届报告")
@SLog(tag = "社团管理系统-信息管理", msg = "删除换届报告")
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);
flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id);
for (ProcessInstance refreshInstance : refreshInstances) {
flowEngine.processInstanceService().deleteProcessInstanceById(refreshInstance.getId());
}
return Result.success();
}
}
@@ -17,6 +17,8 @@ import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
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.IocBean;
import org.nutz.lang.Lang;
@@ -161,6 +163,26 @@ public class SiteCugManageController {
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
@ApiOperation("delete")
@SaCheckPermission("siteCug.manage")
@@ -4,4 +4,12 @@ import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo;
public interface SiteCugInfoService extends BaseService<SiteCugInfo> {
/**
* 仅更新场地开启状态,不触发场地完整表单校验。
*
* @param id 场地ID
* @param state true 表示开启,false 表示关闭
*/
void updateState(String id, Boolean state);
}
@@ -4,7 +4,11 @@ import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo;
import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugInfoService;
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.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean;
@Slf4j
@@ -14,4 +18,16 @@ public class SiteCugInfoServiceImpl extends BaseServiceImpl<SiteCugInfo> impleme
public SiteCugInfoServiceImpl(Dao 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));
}
}
@@ -91,6 +91,7 @@ public class CongressVotingStatisticalController {
@At("/records")
@SaCheckPermission("congress.vote.statistical")
public Result voteRecords(@Param("issueId") String issueId) {
// H5 投票使用现有职代会代表表写入 rep_id,旧表关联仅作为历史数据兼容回退。
Sql sql = Sqls.create("""
SELECT
r.id,
@@ -98,10 +99,17 @@ public class CongressVotingStatisticalController {
r.name,
r.vote_result AS voteResult,
r.create_time AS createTime,
COALESCE(rep.union_id, du.union_id) AS unionId,
du.union_name AS unionName,
COALESCE(rep.delegation_name, d.name) AS delegationName
COALESCE(tc_delegate.unionId, rep.union_id, du.union_id) AS unionId,
COALESCE(tc_delegate.unionName, du.union_name) AS unionName,
COALESCE(delegate_delegation.name, rep.delegation_name, d.name) AS delegationName
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_delegation d ON d.id = r.delegation_id
LEFT JOIN (
@@ -173,10 +173,7 @@ public class ProposalSecondedController {
}
ProposalSearchParam.buildSearch(cnd, pageForm);
cnd.groupBy("t.id");
// 用户未指定有效排序字段时,继续沿用原有任务创建时间倒序。
if (!proposalCommonService.applySafePageOrder(cnd, pageForm, ORDER_COLUMNS)) {
cnd.desc("t.createdAt");
}
cnd.desc("t.createdAt");
sql.setCondition(cnd);
Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination);
@@ -244,7 +244,7 @@ public class ProposalUnderTakeReplyController {
@SaCheckPermission("proposal.unitReply")
@ApiOperation("查询本单位可转办用户")
public Result selectViceUser(@Param("keyword") @Valid String keyword) {
Sql sql = Sqls.create("select id,loginname,username from vw_user $condition");
Sql sql = Sqls.create("select id,loginname,username,unitName from vw_user $condition");
Cnd cnd = Cnd.NEW();
cnd.andEX("unitId", "=", SecurityUtil.getUnitId());
cnd.andEX("id", "!=", SecurityUtil.getUserId());
@@ -79,7 +79,7 @@ public class ProposalSearchParam extends PageForm {
if (StrUtil.isAllNotBlank(searchParam.getPageOrderName(), searchParam.getPageOrderBy())) {
cnd.orderBy(searchParam.getPageOrderName(), PageUtil.getOrder(searchParam.getPageOrderBy()));
} else {
cnd.asc("info.code");
cnd.desc("info.code");
}
}
}
@@ -37,34 +37,13 @@ public class PointsMallInvoiceController {
@At
@SaCheckPermission("points.mall.invoice")
public Result infoPage(PointsMallInvoiceInfoPageParam param) {
try {
return Result.success(pointsMallInvoiceService.infoPage(param));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
return Result.success(pointsMallInvoiceService.infoPage(param));
}
@At
@SaCheckPermission("points.mall.invoice")
public Result infoDetail(@Param("id") String id) {
try {
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());
}
return Result.success(pointsMallInvoiceService.infoDetail(id));
}
@At
@@ -16,9 +16,6 @@ import java.util.Date;
@Comment("积分商城发票信息")
public class PointsMallInvoiceInfo extends BaseModel implements Serializable {
// 详情实时读取供应商 PDF 生成结果:1 成功,2 失败,不以本地文件地址推断状态。
private Integer status;
@Name
@Column
@Comment("ID")
@@ -7,13 +7,11 @@ public class PointsMallInvoiceApplyParam {
private String supplierId;
private String orderIds;
private String queryOrderId;
private String userId;
private String startDate;
private String endDate;
private String invoiceDate;
private String invoiceTitle;
private String taxIdNumber;
private String invoiceCode;
private String invoiceAddress;
private String invoiceContact;
private Integer invoiceType;
@@ -3,9 +3,6 @@ package com.budwk.app.zhgh.pointsmall.invoice.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.BaseService;
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.PointsMallInvoiceInfoPageParam;
import com.budwk.app.zhgh.pointsmall.invoice.param.PointsMallInvoiceMainPageParam;
@@ -18,19 +15,11 @@ import java.util.List;
public interface PointsMallInvoiceService extends BaseService<PointsMallInvoiceMain> {
/** 同步供应商发票信息及本地文件。 */
MallInboundResult invoiceInfo(MallOrderEventDTO dto);
/** 同步发票,可指定是否重新生成失败发票。 */
MallInboundResult invoiceInfo(MallOrderEventDTO dto, boolean retry);
Pagination<PointsMallInvoiceMain> mainPage(PointsMallInvoiceMainPageParam param);
Pagination<Record> infoPage(PointsMallInvoiceInfoPageParam param);
PointsMallInvoiceInfo infoDetail(String id);
void retryInvoice(String mainId);
Record infoDetail(String id);
Pagination<Record> orderPage(PointsMallInvoiceOrderPageParam param);
@@ -6,207 +6,39 @@ import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
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.param.PointsMallInvoiceApplyParam;
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.PointsMallInvoiceOrderPageParam;
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.supplier.models.PointsMallSupplier;
import org.nutz.dao.Chain;
import org.nutz.trans.Atom;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.entity.Record;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.trans.Trans;
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.stream.Collectors;
@Slf4j
@IocBean(args = {"refer:dao"})
public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvoiceMain> implements PointsMallInvoiceService {
@Inject
private MallInvoiceApplyOutboundService mallInvoiceApplyOutboundService;
@Inject
private SysFileService sysFileService;
@Inject
private MallInvoiceInfoQueryService mallInvoiceInfoQueryService;
public PointsMallInvoiceServiceImpl(Dao 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
public Pagination<PointsMallInvoiceMain> mainPage(PointsMallInvoiceMainPageParam param) {
Cnd cnd = Cnd.where("delFlag", "=", false);
if (StrUtil.isNotBlank(param.getSettlementId())) {
cnd.and("settlementId", "=", param.getSettlementId());
cnd.and("settlementId", "like", "%" + param.getSettlementId() + "%");
}
if (param.getBSuccess() != null) {
cnd.and("bSuccess", "=", param.getBSuccess());
@@ -226,32 +58,15 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
@Override
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);
if (StrUtil.isNotBlank(param.getMainId())) {
cnd.and("mainId", "=", param.getMainId());
}
if (StrUtil.isNotBlank(param.getInvoiceId())) {
cnd.and("invoiceId", "=", param.getInvoiceId());
cnd.and("invoiceId", "like", "%" + param.getInvoiceId() + "%");
}
if (StrUtil.isNotBlank(param.getInvoiceCode())) {
cnd.and("invoiceCode", "=", param.getInvoiceCode());
cnd.and("invoiceCode", "like", "%" + param.getInvoiceCode() + "%");
}
if (StrUtil.isNotBlank(param.getInvoiceDate())) {
cnd.and("invoiceDate", "=", DateUtil.parse(param.getInvoiceDate()));
@@ -266,61 +81,13 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
}
@Override
public PointsMallInvoiceInfo infoDetail(String id) {
public Record infoDetail(String id) {
if (StrUtil.isBlank(id)) {
return null;
}
PointsMallInvoiceInfo info = dao().fetch(PointsMallInvoiceInfo.class, Cnd.where("id", "=", id).and("delFlag", "=", false));
if (info == null) {
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(), "供应商发票生成失败"));
}
Sql sql = Sqls.create("select * from points_mall_invoice_info where id=@id and delFlag=0");
sql.params().set("id", id);
return (Record) dao().execute(sql.setCallback(Sqls.callback.record())).getResult();
}
@Override
@@ -350,19 +117,7 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
query.setEndDate(param.getEndDate());
query.setInvoiceStatus(0);
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);
List<PointsMallOrderSub> orders = dao().query(PointsMallOrderSub.class, invoiceOrderCnd(query, ""));
if (CollUtil.isEmpty(orders)) {
throw new IllegalArgumentException("无符合条件的订单数据");
}
@@ -371,69 +126,30 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
String settlementId = System.currentTimeMillis() + RandomUtil.randomNumbers(6);
PointsMallInvoiceMain invoice = new PointsMallInvoiceMain();
invoice.setSettlementId(settlementId);
invoice.setBSuccess(null);
invoice.setSucOrderIds("");
invoice.setBSuccess(0);
invoice.setSucOrderIds(String.join(",", orderIds));
invoice.setFailOrderIds("");
invoice.setFailMsg("");
insert(invoice);
MallInvoiceApplyDTO dto = new MallInvoiceApplyDTO();
dto.setSupplierId(param.getSupplierId());
dto.setSettlementId(settlementId);
dto.setOrderIds(String.join(",", orderIds));
dto.setOrderNum(orders.size());
dto.setOrderTotalPrice(orders.stream().map(PointsMallOrderSub::getTotalPrice)
.filter(java.util.Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add));
dto.setInvoiceDate(param.getInvoiceDate());
dto.setInvoiceTitle(param.getInvoiceTitle());
dto.setTaxIdNumber(param.getTaxIdNumber());
dto.setInvoiceAddress(param.getInvoiceAddress());
dto.setInvoiceContact(param.getInvoiceContact());
dto.setInvoiceType(param.getInvoiceType() == null ? null : String.valueOf(param.getInvoiceType()));
dto.setInvoiceContent(param.getInvoiceContent());
dto.setRemark(param.getRemark());
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());
}
PointsMallInvoiceInfo info = new PointsMallInvoiceInfo();
info.setMainId(invoice.getId());
info.setInvoiceId(settlementId);
info.setInvoiceCode(param.getInvoiceCode());
info.setInvoiceDate(DateUtil.parse(param.getInvoiceDate()));
info.setInvoiceType(param.getInvoiceType());
info.setInvoiceAmount(orderTotalPoints(query));
info.setInvoiceNakeAmount(info.getInvoiceAmount());
info.setInvoiceAddress(param.getInvoiceAddress());
info.setInvoiceContact(param.getInvoiceContact());
info.setInvoiceContent(param.getInvoiceContent());
info.setTaxIdNumber(param.getInvoiceCode());
info.setRemark(param.getRemark());
info.setInvoiceBank(param.getBankName());
info.setInvoiceAccount(param.getBankAccount());
dao().insert(info);
dao().update(PointsMallOrderSub.class, Chain.make("invoiceStatus", 2), Cnd.where("supplierId", "=", param.getSupplierId()).and("orderId", "in", orderIds));
return "开票申请已生成,结算单号:" + settlementId;
}
@@ -454,7 +170,11 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
}
if (StrUtil.isNotBlank(param.getOrderId())) {
List<String> orderIds = StrUtil.splitTrim(param.getOrderId(), ",");
cnd.and(field(alias, "orderId"), "in", orderIds);
if (orderIds.size() > 1) {
cnd.and(field(alias, "orderId"), "in", orderIds);
} else {
cnd.and(field(alias, "orderId"), "like", "%" + param.getOrderId() + "%");
}
}
if (StrUtil.isNotBlank(param.getStartDate())) {
cnd.and(field(alias, "orderCompleteTime"), ">=", DateUtil.beginOfDay(DateUtil.parse(param.getStartDate())));
@@ -476,16 +196,16 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
if (StrUtil.isBlank(param.getSupplierId())) {
throw new IllegalArgumentException("供应商不能为空");
}
if (StrUtil.isBlank(param.getOrderIds())) {
throw new IllegalArgumentException("订单不能为空");
}
if (StrUtil.isBlank(param.getStartDate()) || StrUtil.isBlank(param.getEndDate())) {
throw new IllegalArgumentException("订单完成日期不能为空");
}
if (StrUtil.isBlank(param.getInvoiceDate())) {
throw new IllegalArgumentException("开票日期不能为空");
}
if (StrUtil.isBlank(param.getInvoiceTitle())) {
throw new IllegalArgumentException("发票抬头不能为空");
}
if (StrUtil.isBlank(param.getTaxIdNumber())) {
if (StrUtil.isBlank(param.getInvoiceCode())) {
throw new IllegalArgumentException("纳税人识别号不能为空");
}
if (StrUtil.isBlank(param.getInvoiceAddress())) {
@@ -494,8 +214,5 @@ public class PointsMallInvoiceServiceImpl extends BaseServiceImpl<PointsMallInvo
if (StrUtil.isBlank(param.getInvoiceContact())) {
throw new IllegalArgumentException("联系电话不能为空");
}
if (param.getInvoiceType() == null) {
throw new IllegalArgumentException("发票类型不能为空");
}
}
}
@@ -1,7 +1,5 @@
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.zhgh.pointsmall.mallbridge.annotation.MallTokenRequired;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.*;
@@ -15,7 +13,6 @@ import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.integration.jedis.RedisService;
import org.nutz.mvc.adaptor.JsonAdaptor;
import org.nutz.mvc.annotation.AdaptBy;
import org.nutz.mvc.annotation.At;
@@ -35,25 +32,6 @@ public class PointsMallBridgeController {
@Inject
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
@At("/connect/token")
@ApiOperation(value = "获取 token", notes = "供应商获取访问令牌")
@@ -7,8 +7,6 @@ import java.time.LocalDate;
@Data
public class InvoiceInfoDTO {
private Integer status;
private String remark;
private String invoiceId;
private String invoiceCode;
private LocalDate invoiceDate;
@@ -14,7 +14,7 @@ public class MallInvoiceApplyDTO {
private BigDecimal orderTotalPrice;
private String invoiceDate;
private String invoiceTitle;
private String taxIdNumber;
private String invoiceCode;
private String invoiceAddress;
private String invoiceContact;
private BigDecimal invoiceNakedPrice;
@@ -14,8 +14,5 @@ public class MallOrderEventDTO {
private String deliveryTime;
private String logisticsOrderId;
private String crrgBsnNm;
// 接口推送时间,Unix 毫秒时间戳。
private Long time;
// 本次业务操作发生时间,重推时保留原值,格式 yyyy-MM-dd HH:mm:ss。
private String operationTime;
}
@@ -1,24 +0,0 @@
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;
}
@@ -1,8 +0,0 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.dto;
import lombok.Data;
@Data
public class MallSupplierSessionDTO {
private String keyword;
}
@@ -1,44 +0,0 @@
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,7 +4,6 @@ import lombok.Data;
@Data
public class QueryInvoiceInfoDTO {
private Boolean retry;
private String supplierId;
private String token;
private String settlementId;
@@ -1,28 +0,0 @@
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;
}
@@ -1,15 +0,0 @@
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;
}
@@ -1,22 +0,0 @@
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,7 +7,6 @@ public class UnReconciledMsgNoticeDTO {
private String uniqueSeqNo;
private String supplierId;
private String token;
private Long timestamp;
private String orderIds;
private String unReconciledMsg;
private String unReconciledType;
@@ -3,16 +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.event.MallOrderEventHandler;
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.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.mallbridge.service.MallInboundEventService;
import com.budwk.app.zhgh.pointsmall.order.enums.MallOrderStatusEnum;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -21,44 +12,19 @@ import org.nutz.ioc.loader.annotation.IocBean;
* MallAbolishSubOrderEventHandler
* orderType=8: 废除子订单事件处理器。
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class MallAbolishSubOrderEventHandler extends AbstractMallBridgeSupport implements MallOrderEventHandler {
@IocBean
public class MallAbolishSubOrderEventHandler implements MallOrderEventHandler {
@Inject
private PointsMallPointsLockService pointsMallPointsLockService;
public MallAbolishSubOrderEventHandler(Dao dao) {
super(dao);
}
private MallInboundEventService inboundEventService;
@Override
public int getType() {
return 8;
}
/**
* 废除指定子订单,在同一事务中更新状态并释放或退回积分。
*/
@Override
public MallInboundResult handle(MallOrderEventDTO dto) {
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();
return inboundEventService.updateSubOrderStatus(dto, MallOrderStatusEnum.ABOLISH);
}
}
@@ -3,18 +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.event.MallOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
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 com.budwk.app.zhgh.pointsmall.mallbridge.service.MallInboundEventService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -22,50 +11,19 @@ import org.nutz.ioc.loader.annotation.IocBean;
* MallCancelPayedMainOrderEventHandler
* orderType=7: 已支付未拆单的整个父单取消。
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class MallCancelPayedMainOrderEventHandler extends AbstractMallBridgeSupport implements MallOrderEventHandler {
@IocBean
public class MallCancelPayedMainOrderEventHandler implements MallOrderEventHandler {
@Inject
private PointsMallPointsLockService pointsMallPointsLockService;
public MallCancelPayedMainOrderEventHandler(Dao dao) {
super(dao);
}
private MallInboundEventService inboundEventService;
@Override
public int getType() {
return 7;
}
/**
* 处理已支付未拆单主单取消通知,沿用原 paidNotSplit=true 分支的校验与积分处理。
*/
@Override
public MallInboundResult handle(MallOrderEventDTO dto) {
// 优先使用 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();
return inboundEventService.cancelMainOrder(dto, true);
}
}
@@ -3,18 +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.event.MallOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
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 com.budwk.app.zhgh.pointsmall.mallbridge.service.MallInboundEventService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -22,54 +11,19 @@ import org.nutz.ioc.loader.annotation.IocBean;
* MallCancelPendingOrderEventHandler
* orderType=1: 取消待支付主单事件处理器。
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class MallCancelPendingOrderEventHandler extends AbstractMallBridgeSupport implements MallOrderEventHandler {
@IocBean
public class MallCancelPendingOrderEventHandler implements MallOrderEventHandler {
@Inject
private PointsMallPointsLockService pointsMallPointsLockService;
public MallCancelPendingOrderEventHandler(Dao dao) {
super(dao);
}
private MallInboundEventService inboundEventService;
@Override
public int getType() {
return 1;
}
/**
* 处理待支付主单取消通知,校验待支付状态后取消订单并释放主单积分锁。
*/
@Override
public MallInboundResult handle(MallOrderEventDTO dto) {
// 优先使用 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();
return inboundEventService.cancelMainOrder(dto, false);
}
}
@@ -1,8 +1,6 @@
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.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.sys.models.Sys_user;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderEventDTO;
@@ -63,7 +61,7 @@ public class MallCancelSubOrderEventHandler implements MallOrderEventHandler {
if (dto == null || StrUtil.hasBlank(dto.getSupplierId(), dto.getOrderId(), dto.getUserId())) {
return MallInboundResult.fail(400, "供应商、订单、用户不能为空");
}
// 供应商传入 userId 为虚拟用户ID,转换为系统用户后用于消息推送
// userId 兼容系统用户ID和工号,后续消息推送需要使用用户信息
Sys_user user = fetchUser(dto.getUserId());
if (user == null) {
return MallInboundResult.fail(-1, "用户不存在");
@@ -104,10 +102,7 @@ public class MallCancelSubOrderEventHandler implements MallOrderEventHandler {
pointsMallOrderService.updateMainOrderStatus(sub.getMainOrderId(), sub.getSupplierId());
});
try {
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);
mallCallPMessagePushService.pushMessageToGlobal(StrUtil.blankToDefault(user.getUsername(), user.getLoginname()), sub);
} catch (Exception e) {
log.error("9.2.1 子单取消,callP消息推送:{},{}", dto, e.getMessage());
}
@@ -115,8 +110,14 @@ public class MallCancelSubOrderEventHandler implements MallOrderEventHandler {
return MallInboundResult.success();
}
private Sys_user fetchUser(String virtualId) {
return dao.fetch(Sys_user.class, Cnd.where("virtual_id", "=", virtualId)
.and("disabled", "=", false).and("delFlag", "=", false));
/**
* 入参 userId 可能是系统用户ID或工号,按当前项目用户表兼容查询。
*/
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,26 +1,9 @@
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.event.MallOrderEventHandler;
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.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.mallbridge.service.MallInboundEventService;
import com.budwk.app.zhgh.pointsmall.order.enums.MallOrderStatusEnum;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -33,103 +16,19 @@ import org.nutz.ioc.loader.annotation.IocBean;
* <p>注意供应商侧确认收货对应 zhgh/callP 已妥投(4)
* 不等同于 zhgh/callP 用户确认收货后的已完成(5)</p>
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class MallConfirmReceiptEventHandler extends AbstractMallBridgeSupport implements MallOrderEventHandler {
@IocBean
public class MallConfirmReceiptEventHandler implements MallOrderEventHandler {
@Inject
private MallOrderQueryOutboundService mallOrderQueryOutboundService;
@Inject
private MallCallPMessagePushService mallCallPMessagePushService;
public MallConfirmReceiptEventHandler(Dao dao) {
super(dao);
}
private MallInboundEventService inboundEventService;
@Override
public int getType() {
return 6;
}
/**
* 处理供应商确认收货通知仅更新为已妥投不执行用户确认完成后的积分扣减
*/
@Override
public MallInboundResult handle(MallOrderEventDTO dto) {
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());
}
return inboundEventService.updateSubOrderStatus(dto, MallOrderStatusEnum.RECEIVED);
}
}
@@ -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.event.MallOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.zhgh.pointsmall.invoice.service.PointsMallInvoiceService;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.MallInboundEventService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -15,7 +15,7 @@ import org.nutz.ioc.loader.annotation.IocBean;
public class MallInvoiceInfoEventHandler implements MallOrderEventHandler {
@Inject
private PointsMallInvoiceService pointsMallInvoiceService;
private MallInboundEventService inboundEventService;
@Override
public int getType() {
@@ -24,6 +24,6 @@ public class MallInvoiceInfoEventHandler implements MallOrderEventHandler {
@Override
public MallInboundResult handle(MallOrderEventDTO dto) {
return pointsMallInvoiceService.invoiceInfo(dto);
return inboundEventService.invoiceInfo(dto);
}
}
@@ -3,116 +3,27 @@ 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.event.MallOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import cn.hutool.core.util.StrUtil;
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 com.budwk.app.zhgh.pointsmall.mallbridge.service.MallInboundEventService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
/**
* MallShipOrderEventHandler
* orderType=99: 供应商已发货 / 更新发货状态
* 在处理器内完成订单校验及主子订单物流信息更新不再回调事件分发服务
* 本事件仅更新发货状态和物流信息不修改订单业务状态 orderState
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class MallShipOrderEventHandler extends AbstractMallBridgeSupport implements MallOrderEventHandler {
@IocBean
public class MallShipOrderEventHandler implements MallOrderEventHandler {
public MallShipOrderEventHandler(Dao dao) {
super(dao);
}
@Inject
private MallInboundEventService inboundEventService;
@Override
public int getType() {
return 99;
}
/**
* 处理供应商发货通知在同一事务中更新匹配的子订单及一个主订单
*
* @param dto 发货事件orderId 为英文逗号分隔的子订单号userId 为可选的用户虚拟 ID
* @return 参数或用户映射无效时返回 400无匹配子订单时返回 404更新完成返回成功
*/
@Override
public MallInboundResult handle(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())) {
// 供应商传入虚拟 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();
return inboundEventService.updateDeliveryStatus(dto);
}
}
@@ -94,7 +94,7 @@ public class MallSplitSubOrderEventHandler implements MallOrderEventHandler {
// 调用外部服务获取子订单信息
MallOutboundResult result = mallOrderQueryOutboundService.orderQuery(queryDTO);
if (result == null || result.getResultCode() != 0) {
return MallInboundResult.fail(result == null ? 502 : result.getResultCode(), result == null ? "获取订单详情返回为空" : result.getMessage());
throw new RuntimeException(result == null ? "获取订单详情返回为空" : result.getMessage());
}
orderInfoList = result.getOrderInfo();
} catch (Exception e) {
@@ -1,5 +1,4 @@
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;
@@ -149,15 +148,13 @@ public class MallSubOrderReturnEventHandler extends AbstractMallBridgeSupport im
});
try {
// callP消息推送
MallOrderMessageDTO notifySub = BeanUtil.copyProperties(sub, MallOrderMessageDTO.class);
notifySub.setOperationTime(parseDate(dto.getOperationTime()));
PointsMallOrderSub notifySub = BeanUtil.copyProperties(sub, PointsMallOrderSub.class);
notifySub.setOrderId(dto.getOrderId());
notifySub.setPointsPrice(refund);
notifySub.setTotalPrice(orderInfo.getTotalPrice());
notifySub.setWPayOrAPay(orderInfo.getWPayOrAPay());
notifySub.setExtJson(orderExchange.getExtJson());
notifySub.setUserName(StrUtil.blankToDefault(user.getUsername(), user.getLoginname()));
mallCallPMessagePushService.pushMessageToGlobal("66666666", notifySub);
mallCallPMessagePushService.pushMessageToGlobal(StrUtil.blankToDefault(user.getUsername(), user.getLoginname()), notifySub);
} catch (Exception e) {
log.error("9.2.1 子单取消 或 退货,callP消息推送: {}, {}", dto, e.getMessage());
}
@@ -1,55 +0,0 @@
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,7 +8,8 @@ import com.budwk.app.zhgh.dayofficework.message.service.GlobalMessageSendService
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.PointsMallMessageTemplate;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderMessageDTO;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderMain;
import com.budwk.app.zhgh.pointsmall.order.models.PointsMallOrderSub;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Cnd;
@@ -38,111 +39,123 @@ public class MallCallPMessagePushService {
}
/**
* 统一订单消息入口11111111 支付成功22222222 妥投提醒66666666 积分退回
* 主单子单均转换为 MallOrderMessageDTO退货传入退货单号及本次退款积分
* 已成功消息不重复发送失败记录可在再次调用时重用
* synchronized 防止当前服务实例内的并发通知重复创建消息
* 支付成功callP消息推送
*
* @param mainOrder 订单信息
*/
public synchronized void pushMessageToGlobal(String templateId, MallOrderMessageDTO order) {
if (order == null || StrUtil.hasBlank(templateId, order.getSupplierId(), order.getUserId(), order.getOrderId())) {
throw new IllegalArgumentException("消息模板、供应商、用户和订单不能为空");
}
if ("66666666".equals(templateId) && nvl(order.getPointsPrice()).signum() <= 0) {
public void pushMessageToGlobal(PointsMallOrderMain mainOrder) {
PointsMallMessageTemplate template = dao.fetch(PointsMallMessageTemplate.class, Cnd.where("id", "=", "11111111").and("delFlag", "=", false));
if (template == null) {
log.info("支付成功推送消息失败,未配置消息模板");
return;
}
String userId = order.getUserId();
Date now = new Date();
Date finishTime = "11111111".equals(templateId) ? order.getOrderCreateTime()
: order.getOrderFinishTime() == null ? now : order.getOrderFinishTime();
Date planPushTime = now;
String uniqueNo = templateId + "-" + order.getSupplierId() + "-" + order.getOrderId();
if ("22222222".equals(templateId)) {
// 妥投提醒按自然日去重取消退货按业务订单号去重
if (DateUtil.beginOfDay(finishTime).after(DateUtil.beginOfDay(now))) {
return;
}
long day = DateUtil.betweenDay(DateUtil.beginOfDay(finishTime), DateUtil.beginOfDay(now), false);
uniqueNo += "-" + day;
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);
Sys_user user = dao.fetch(Sys_user.class, Cnd.where("id", "=", mainOrder.getUserId()).or("loginname", "=", mainOrder.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;
}
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());
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 : params.entrySet()) {
content = content.replace(entry.getKey(), StrUtil.blankToDefault(entry.getValue(), ""));
}
boolean create = message == null;
if (create) {
message = new PointsMallMessage();
message.setUniqueNo(uniqueNo);
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(templateId);
message.setTouser(userId);
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 finishTime = subOrder.getOrderFinishTime() == null ? now : subOrder.getOrderFinishTime();
long day = DateUtil.betweenDay(DateUtil.beginOfDay(finishTime), DateUtil.beginOfDay(now), false);
String uniqueNo = template.getId().concat("-").concat(subOrder.getSupplierId()).concat("-")
.concat(subOrder.getOrderId()).concat("-").concat(String.valueOf(day));
StringBuilder sb = new StringBuilder();
sb.append("商品列表: ").append(System.lineSeparator());
List<OrderProInfoDTO> productList = StrUtil.isBlank(subOrder.getExtJson())
? 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());
}
Date planPushTime = DateUtil.offsetDay(finishTime, (int) day);
PointsMallMessage message = new PointsMallMessage();
message.setUniqueNo(uniqueNo);
message.setPushType("CALLP");
message.setTemplateId(template.getId());
message.setTouser(subOrder.getUserId());
message.setContent(content);
message.setContentLink(template.getContentLink());
message.setPlanPushTime(planPushTime);
message.setStatus(0);
if (create) {
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);
}
}
dao.insert(message);
/** 公共模板变量:用户姓名、供应商名称和订单商品列表。 */
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;
globalMessageSendService.sendLocalSystemMessage(template.getName(), content, 1, Collections.singletonList(subOrder.getUserId()), null);
}
private String formatMoney(BigDecimal value) {
@@ -1,6 +1,11 @@
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.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.handlers.MallAbolishSubOrderEventHandler;
import com.budwk.app.zhgh.pointsmall.mallbridge.event.handlers.MallCancelPayedMainOrderEventHandler;
@@ -13,15 +18,28 @@ 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.MallSubOrderReturnEventHandler;
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.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 lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
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.List;
import java.util.Map;
import com.budwk.app.sys.services.SysFileService;
/**
* 商城桥接订单事件入站服务
@@ -35,10 +53,6 @@ public class MallInboundEventService extends AbstractMallBridgeSupport {
@Inject
private MallOrderQueryOutboundService mallOrderQueryOutboundService;
@Inject
private MallInvoiceInfoQueryService mallInvoiceInfoQueryService;
@Inject
private SysFileService sysFileService;
@Inject
private MallCancelPendingOrderEventHandler mallCancelPendingOrderEventHandler;
@Inject
private MallSplitSubOrderEventHandler mallSplitSubOrderEventHandler;
@@ -103,4 +117,307 @@ public class MallInboundEventService extends AbstractMallBridgeSupport {
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,7 +2,6 @@ package com.budwk.app.zhgh.pointsmall.mallbridge.service;
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.MallPendingOrderDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallInboundResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport;
@@ -33,17 +32,13 @@ public class MallInboundPendingOrderService extends AbstractMallBridgeSupport {
if (dto.getOrderInfo() == null || StrUtil.isBlank(dto.getOrderInfo().getOrderId())) {
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());
if (dao.count(PointsMallOrderMain.class, Cnd.where("orderId", "=", dto.getOrderInfo().getOrderId())
.and("supplierId", "=", dto.getSupplierId()).and("delFlag", "=", false)) > 0) {
return MallInboundResult.fail(-1, "订单已存在");
}
Trans.exec((Atom) () -> {
PointsMallOrderMain main = toMain(dto.getSupplierId(), user.getId(), dto.getOrderInfo(), ORDER_PENDING_PAY);
PointsMallOrderMain main = toMain(dto.getSupplierId(), dto.getUserId(), dto.getOrderInfo(), ORDER_PENDING_PAY);
dao.insert(main);
// 新增主单历史记录
@@ -1,13 +1,9 @@
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.model.MallInboundPointsResult;
import com.budwk.app.zhgh.pointsmall.mallbridge.service.support.AbstractMallBridgeSupport;
import com.budwk.app.zhgh.pointsmall.points.service.PointsMallUserPointsService;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
@@ -15,28 +11,17 @@ import org.nutz.ioc.loader.annotation.IocBean;
* 商城桥接积分查询服务
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class MallInboundPointsQueryService extends AbstractMallBridgeSupport {
@IocBean
public class MallInboundPointsQueryService {
@Inject
private PointsMallUserPointsService pointsMallUserPointsService;
public MallInboundPointsQueryService(Dao dao) {
super(dao);
}
/**
* 查询用户可用积分
*/
public MallInboundPointsResult queryPoints(MallPointQueryDTO dto) {
log.info("积分商城查询用户积分,supplierId={}userId={}", dto == null ? null : dto.getSupplierId(), dto == null ? null : 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()));
return MallInboundPointsResult.success(pointsMallUserPointsService.availablePoints(dto.getUserId()));
}
}
@@ -1,5 +1,4 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.service;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallOrderMessageDTO;
import cn.hutool.core.bean.BeanUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.MallPurchaseResultDTO;
@@ -59,7 +58,7 @@ public class MallInboundPurchaseResultService {
}
try {
// callP消息推送
mallCallPMessagePushService.pushMessageToGlobal("11111111", BeanUtil.copyProperties(orderMain, MallOrderMessageDTO.class));
mallCallPMessagePushService.pushMessageToGlobal(orderMain);
} catch (Exception e) {
log.error("9.2.14 支付结果通知接口,callP消息推送:{},{}", dto, e.getMessage());
}
@@ -1,22 +1,24 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.service;
import cn.hutool.http.HttpUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.budwk.app.zhgh.pointsmall.mallbridge.util.MallBridgeResponseUtil;
import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceInfo;
import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceMain;
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.service.support.AbstractMallBridgeSupport;
import com.budwk.app.zhgh.pointsmall.mallbridge.util.MallBridgeCryptoUtil;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
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.ioc.impl.PropertiesProxy;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.trans.Atom;
import org.nutz.trans.Trans;
import java.util.HashMap;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
/**
* 商城桥接开票申请出站服务
@@ -25,61 +27,73 @@ import java.util.HashMap;
@IocBean(args = {"refer:dao"})
public class MallInvoiceApplyOutboundService extends AbstractMallBridgeSupport {
@Inject
private PropertiesProxy conf;
public MallInvoiceApplyOutboundService(Dao dao) {
super(dao);
}
/**
* 向供应商发起开票申请
* 接收开票申请并保存开票记录
*/
public MallOutboundResult applyInvoice(MallInvoiceApplyDTO dto) {
if (dto == null || StrUtil.hasBlank(dto.getSupplierId(), dto.getSettlementId(), dto.getOrderIds())) {
return MallOutboundResult.fail(400, "开票申请参数不能为空");
}
String clientBAB7ServerUrl = conf.get("spdb.bab7.url", "");
if (StrUtil.isBlank(clientBAB7ServerUrl)) {
return MallOutboundResult.fail(500, "未配置 BAB7 服务地址");
List<String> orderIds = splitIds(dto.getOrderIds());
log.info("积分商城开票申请开始,supplierId={}settlementId={}orderCount={}", dto.getSupplierId(), dto.getSettlementId(), orderIds.size());
List<PointsMallOrderSub> orders = dao.query(PointsMallOrderSub.class, Cnd.where("supplierId", "=", dto.getSupplierId())
.and("orderId", "in", orderIds).and("delFlag", "=", false));
if (orders.isEmpty()) {
return MallOutboundResult.fail(404, "未找到可开票订单");
}
PointsMallSupplier supplier = dao.fetch(PointsMallSupplier.class,
Cnd.where("supplierId", "=", dto.getSupplierId()).and("delFlag", "=", false));
if (supplier == null) {
return MallOutboundResult.fail(404, "未找到对应供应商信息");
if (dto.getOrderNum() != null && dto.getOrderNum() != orders.size()) {
return MallOutboundResult.fail(400, "订单数量与开票申请不一致");
}
if (StrUtil.isBlank(supplier.getSupplierApiUrl())) {
return MallOutboundResult.fail(404, "供应商地址未配置");
BigDecimal total = orders.stream().map(PointsMallOrderSub::getTotalPrice).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
if (dto.getOrderTotalPrice() != null && total.compareTo(dto.getOrderTotalPrice()) != 0) {
return MallOutboundResult.fail(400, "订单金额与开票申请不一致");
}
long timestamp = System.currentTimeMillis();
dto.setToken(MallBridgeCryptoUtil.generateSign(supplier.getSupplierId(), supplier.getClientId(), timestamp));
dto.setTimestamp(timestamp);
Trans.exec((Atom) () -> saveInvoiceApply(dto, orderIds, total));
log.info("积分商城开票申请完成,supplierId={}settlementId={}", dto.getSupplierId(), dto.getSettlementId());
return MallOutboundResult.success();
}
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) {
PointsMallInvoiceMain main = dao.fetch(PointsMallInvoiceMain.class, Cnd.where("settlementId", "=", dto.getSettlementId()).and("delFlag", "=", false));
if (main == null) {
main = new PointsMallInvoiceMain();
main.setSettlementId(dto.getSettlementId());
main.setBSuccess(0);
main.setSucOrderIds(String.join(",", orderIds));
main.setFailOrderIds("");
main.setFailMsg("");
dao.insert(main);
} else {
main.setBSuccess(0);
main.setSucOrderIds(String.join(",", orderIds));
main.setFailOrderIds("");
main.setFailMsg("");
dao.updateIgnoreNull(main);
}
HashMap<String, Object> bodyMap = new HashMap<>();
bodyMap.put("Address4", supplier.getSupplierApiUrl() + "/supplierOpenApi/pointOrder/applyForInvoices");
bodyMap.put("BussDealMd", "P");
bodyMap.put("SroNo", supplier.getSroNo());
bodyMap.put("RsrvFld1", supplier.getClientId());
bodyMap.put("AplParmObjct", encrypt);
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);
String response = HttpUtil.createPost(clientBAB7ServerUrl)
.contentType("application/json;charset=UTF-8")
.body(JSONUtil.toJsonStr(bodyMap))
.execute()
.body();
if (StrUtil.isBlank(response)) {
return MallOutboundResult.fail(502, "供应商开票申请无响应");
}
return JSONUtil.toBean(MallBridgeResponseUtil.parse(response), MallOutboundResult.class);
dao.update(PointsMallOrderSub.class, Chain.make("invoiceStatus", 2).add("invoiceCode", dto.getSettlementId()),
Cnd.where("supplierId", "=", dto.getSupplierId()).and("orderId", "in", orderIds));
}
}
@@ -1,26 +1,19 @@
package com.budwk.app.zhgh.pointsmall.mallbridge.service;
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.util.MallBridgeResponseUtil;
import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceInfo;
import com.budwk.app.zhgh.pointsmall.invoice.models.PointsMallInvoiceMain;
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.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 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;
import java.util.List;
import com.budwk.app.zhgh.pointsmall.mallbridge.dto.PictureQueryDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallPictureOutboundResult;
import java.util.stream.Collectors;
/**
* 商城桥接发票信息查询服务
@@ -29,51 +22,10 @@ import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallPictureOutboundResult;
@IocBean(args = {"refer:dao"})
public class MallInvoiceInfoQueryService extends AbstractMallBridgeSupport {
@Inject
private PropertiesProxy conf;
public MallInvoiceInfoQueryService(Dao 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);
}
/**
* 查询指定结算单的发票信息
*/
@@ -81,57 +33,21 @@ public class MallInvoiceInfoQueryService extends AbstractMallBridgeSupport {
if (dto == null || StrUtil.hasBlank(dto.getSupplierId(), dto.getSettlementId())) {
throw new IllegalArgumentException("发票查询参数不能为空");
}
String clientBAB7ServerUrl = conf.get("spdb.bab7.url", "");
if (StrUtil.isBlank(clientBAB7ServerUrl)) {
throw new IllegalArgumentException("未配置 BAB7 服务地址");
log.info("积分商城查询发票信息,supplierId={}settlementId={}", dto.getSupplierId(), dto.getSettlementId());
PointsMallInvoiceMain main = dao.fetch(PointsMallInvoiceMain.class, Cnd.where("settlementId", "=", dto.getSettlementId())
.and("delFlag", "=", false));
if (main == null) {
throw new IllegalArgumentException("发票信息不存在");
}
PointsMallSupplier supplier = dao.fetch(PointsMallSupplier.class,
Cnd.where("supplierId", "=", dto.getSupplierId()).and("delFlag", "=", false));
if (supplier == null) {
throw new IllegalArgumentException("未找到对应供应商信息");
}
if (StrUtil.isBlank(supplier.getSupplierApiUrl())) {
throw new IllegalArgumentException("供应商地址未配置");
}
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);
List<InvoiceInfoDTO> invoiceInfos = dao.query(PointsMallInvoiceInfo.class, Cnd.where("mainId", "=", main.getId()).and("delFlag", "=", false))
.stream().map(this::toInvoiceInfoDTO).collect(Collectors.toList());
GetInvoiceInfoDTO result = new GetInvoiceInfoDTO();
result.setBSuccess(main.getBSuccess());
result.setSettlementId(main.getSettlementId());
result.setSucOrderIds(main.getSucOrderIds());
result.setFailOrderIds(main.getFailOrderIds());
result.setFailMsg(main.getFailMsg());
result.setInvoiceInfos(invoiceInfos);
return result;
}
}
@@ -2,7 +2,6 @@ package com.budwk.app.zhgh.pointsmall.mallbridge.service;
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.MallOrderQueryDTO;
import com.budwk.app.zhgh.pointsmall.mallbridge.model.MallOutboundResult;
@@ -86,31 +85,6 @@ public class MallOrderQueryOutboundService {
.execute()
.body();
log.info("9.2.2 获取订单详情调用结束,返回报文:{}", response);
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);
return JSONUtil.toBean(response, MallOutboundResult.class);
}
}
@@ -1,185 +0,0 @@
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;
}
}
@@ -1,70 +0,0 @@
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,36 +2,25 @@ package com.budwk.app.zhgh.pointsmall.mallbridge.service;
import cn.hutool.core.convert.Convert;
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.model.MallInboundResult;
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.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 org.nutz.trans.Atom;
import org.nutz.trans.Trans;
import java.util.HashMap;
import java.util.List;
/**
* 对账异常消息通知服务
* 商城桥接对账异常通知服务
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class MallUnReconciledMsgNoticeService extends AbstractMallBridgeSupport {
@Inject
private PropertiesProxy conf;
public MallUnReconciledMsgNoticeService(Dao dao) {
super(dao);
}
@@ -61,61 +50,4 @@ public class MallUnReconciledMsgNoticeService extends AbstractMallBridgeSupport
log.info("积分商城对账异常通知完成,supplierId={}orderCount={}", dto.getSupplierId(), orderIds.size());
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,105 +34,43 @@ import java.util.stream.Collectors;
* <p>只放通用转换和常量具体接口业务放在各自服务中</p>
*/
public abstract class AbstractMallBridgeSupport {
/**
* 访问令牌有效期单位为秒1 小时
*/
protected static final long ACCESS_EXPIRE_SECONDS = 3600;
/**
* 刷新令牌有效期单位为秒7
*/
protected static final long REFRESH_EXPIRE_SECONDS = 86400L * 7;
/**
* 订单状态待支付
*/
protected static final int ORDER_PENDING_PAY = 0;
/**
* 订单状态已支付
*/
protected static final int ORDER_FINISH_PAY = 1;
/**
* 订单状态处理中
*/
protected static final int ORDER_IN_PROGRESS = 2;
/**
* 订单状态已取消
*/
protected static final int ORDER_CANCELLED = 3;
/**
* 订单状态已收货
*/
protected static final int ORDER_RECEIVED = 4;
/**
* 订单状态已完成
*/
protected static final int ORDER_COMPLETED = 5;
/**
* 订单状态退换货
*/
protected static final int ORDER_EXCHANGE = 6;
/**
* 订单状态已退款
*/
protected static final int ORDER_REFUNDED = 7;
/**
* 订单状态已作废
*/
protected static final int ORDER_ABOLISH = 8;
/**
* 供桥接服务查询和保存用户订单商品等数据的 DAO
*/
protected final Dao dao;
/**
* 初始化桥接公共支撑类复用业务服务传入的 DAO
*/
protected AbstractMallBridgeSupport(Dao dao) {
this.dao = dao;
}
/**
* 检查订单事件及供应商订单主订单用户标识是否齐全不校验对应数据是否存在
*/
protected boolean hasRequiredOrderEventFields(MallOrderEventDTO dto) {
return dto != null && !StrUtil.hasBlank(dto.getSupplierId(), dto.getOrderId(), dto.getMainOrderId(), dto.getUserId());
}
/**
* 根据对外用户虚拟 ID 查询未禁用未删除的系统用户
*
* @param virtualId 供应商接口传入的用户虚拟 ID不是系统用户主键
* @return 符合条件的用户虚拟 ID 为空或用户不存在时返回 null
*/
protected Sys_user fetchUser(String virtualId) {
if (StrUtil.isBlank(virtualId)) {
return null;
protected 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("virtual_id", "=", virtualId)
.and("disabled", "=", false).and("delFlag", "=", false));
return dao.fetch(Sys_user.class, Cnd.where("loginname", "=", userId).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) {
Sys_user user = fetchUser(dto.getUserId());
if (user == null) {
throw new IllegalArgumentException("用户虚拟ID未配置或不存在");
}
PointsMallOrderExchange exchange = new PointsMallOrderExchange();
exchange.setOrderMainId(dto.getMainOrderId());
exchange.setOrderId(dto.getOrderId());
exchange.setSupplierId(dto.getSupplierId());
exchange.setUserId(user.getId());
exchange.setUserId(dto.getUserId());
exchange.setAplName(orderInfo.getAplName());
exchange.setOrderCreateTime(parseDate(orderInfo.getCreateTime()));
exchange.setOrderUpdateTime(parseDate(orderInfo.getUpdateTime()));
@@ -152,9 +90,6 @@ public abstract class AbstractMallBridgeSupport {
return exchange;
}
/**
* SKU 将原子单的图片及文件信息覆盖到传入商品列表中原子单重复 SKU 取首项传入列表为空时返回空列表
*/
protected List<OrderProInfoDTO> mergeProductInfo(List<OrderProInfoDTO> products, String subExtJson) {
if (products == null || products.isEmpty()) {
return Collections.emptyList();
@@ -176,15 +111,6 @@ public abstract class AbstractMallBridgeSupport {
return products;
}
/**
* 将供应商订单详情转换为主订单金额为空时按零处理现金支付标记初始化为 0
*
* @param supplierId 供应商标识
* @param userId 已由虚拟 ID 查得的系统用户主键
* @param orderInfo 供应商返回的订单详情
* @param state 主订单状态
* @return 尚未入库的主订单
*/
protected PointsMallOrderMain toMain(String supplierId, String userId, OrderInfoDTO orderInfo, Integer state) {
PointsMallOrderMain main = new PointsMallOrderMain();
main.setOrderId(orderInfo.getOrderId());
@@ -212,9 +138,6 @@ public abstract class AbstractMallBridgeSupport {
return main;
}
/**
* 从主订单复制信息生成子订单子订单号为空时使用主订单号更新时间取当前时间对账和发票状态初始化为 0
*/
protected PointsMallOrderSub toSub(PointsMallOrderMain main, String subOrderId) {
PointsMallOrderSub sub = new PointsMallOrderSub();
sub.setOrderId(StrUtil.blankToDefault(subOrderId, main.getOrderId()));
@@ -244,9 +167,6 @@ public abstract class AbstractMallBridgeSupport {
return sub;
}
/**
* 将商品逐条保存到指定子订单仅保存图片列表中的首张图片商品列表为 null 时不处理
*/
protected void insertProducts(String orderSubId, List<OrderProInfoDTO> products) {
if (products == null) {
return;
@@ -264,9 +184,6 @@ public abstract class AbstractMallBridgeSupport {
}
}
/**
* 将查询记录转换为对外订单详情统一日期和金额格式并读取订单商品信息
*/
protected OrderInfoDTO toOrderInfo(Record record) {
OrderInfoDTO dto = new OrderInfoDTO();
dto.setOrderId(record.getString("orderId"));
@@ -294,9 +211,6 @@ public abstract class AbstractMallBridgeSupport {
return dto;
}
/**
* 优先读取订单下未删除的子单商品无商品记录时从 extJson 解析扩展信息为空或为 null 字符串时返回空列表
*/
protected List<OrderProInfoDTO> readProducts(String orderId, String extJson) {
List<PointsMallOrderSubProduct> products = dao.query(PointsMallOrderSubProduct.class, Cnd.where("orderSubId", "=", orderId).and("delFlag", "=", false));
if (!products.isEmpty()) {
@@ -308,9 +222,6 @@ public abstract class AbstractMallBridgeSupport {
return JSONUtil.toList(extJson, OrderProInfoDTO.class);
}
/**
* 将子单商品转换为对外商品信息单张图片包装为列表并以 SKU 和图片地址填充文件标识及路径
*/
protected OrderProInfoDTO toProductDTO(PointsMallOrderSubProduct product) {
OrderProInfoDTO dto = new OrderProInfoDTO();
dto.setSku(product.getSku());
@@ -324,9 +235,6 @@ public abstract class AbstractMallBridgeSupport {
return dto;
}
/**
* 将发票记录转换为对外发票信息并将开票时间转换为本地日期
*/
protected InvoiceInfoDTO toInvoiceInfoDTO(PointsMallInvoiceInfo info) {
InvoiceInfoDTO dto = new InvoiceInfoDTO();
dto.setInvoiceId(info.getInvoiceId());
@@ -345,9 +253,6 @@ public abstract class AbstractMallBridgeSupport {
return dto;
}
/**
* 解析日期字符串将日期时间分隔符 T 替换为空格空白值返回 null非法日期由日期工具抛出异常
*/
protected Date parseDate(String value) {
if (StrUtil.isBlank(value)) {
return null;
@@ -355,9 +260,6 @@ public abstract class AbstractMallBridgeSupport {
return DateUtil.parse(value.replace("T", " "));
}
/**
* 将日期值统一格式化为 yyyy-MM-dd HH:mm:ssnull 值返回 null
*/
protected String formatDate(Object value) {
if (value == null) {
return null;
@@ -365,16 +267,10 @@ public abstract class AbstractMallBridgeSupport {
return DateUtil.format(Convert.toDate(value), "yyyy-MM-dd HH:mm:ss");
}
/**
* 使用系统默认时区将时间转换为日期null 值返回 null
*/
protected LocalDate toLocalDate(Date date) {
return date == null ? null : date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
/**
* 拆分逗号分隔的标识去除首尾空白空项和重复项保留首次出现的顺序空白值返回空列表
*/
protected List<String> splitIds(String value) {
if (StrUtil.isBlank(value)) {
return Collections.emptyList();
@@ -386,30 +282,18 @@ public abstract class AbstractMallBridgeSupport {
.collect(Collectors.toList());
}
/**
* 金额为 null 时返回零否则保留原值
*/
protected BigDecimal nvl(BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
/**
* 金额为 null 时返回指定默认值否则保留原值
*/
protected BigDecimal nvl(BigDecimal value, BigDecimal defaultValue) {
return value == null ? defaultValue : value;
}
/**
* 将对象转换为金额无法转换或值为 null 时使用零作为默认值
*/
protected BigDecimal decimal(Object value) {
return Convert.toBigDecimal(value, BigDecimal.ZERO);
}
/**
* 将字符串转换为整数空白值或无法转换的值返回 null
*/
protected Integer parseInteger(String value) {
if (StrUtil.isBlank(value)) {
return null;
@@ -1,37 +0,0 @@
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,98 +2,33 @@ package com.budwk.app.zhgh.pointsmall.order.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
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.OrderInfoDTO;
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.models.PointsMallOrderSub;
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.service.PointsMallOrderSubHistoryService;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.Collections;
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"})
public class PointsMallOrderSubHistoryServiceImpl extends BaseServiceImpl<PointsMallOrderSubHistory> implements PointsMallOrderSubHistoryService {
@Inject
private MallInvoiceInfoQueryService mallInvoiceInfoQueryService;
@Inject
private SysFileService sysFileService;
public PointsMallOrderSubHistoryServiceImpl(Dao dao) {
super(dao);
}
@Override
public List<OrderProInfoDTO> getPictureBase64(String supplierId, String orderId, String queryType, List<OrderProInfoDTO> 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);
}
return CollUtil.isEmpty(orderProInfo) ? Collections.emptyList() : orderProInfo;
}
@Override
@@ -48,11 +48,15 @@ public class PointsMallReconciliationController {
}
}
@At("/execute")
@At
@SLog(tag = "积分商城对账管理", msg = "执行订单对账")
@SaCheckPermission("points.mall.reconciliation")
public Result execute(PointsMallReconciliationParam param) {
return pointsMallReconciliationService.executeReconciliation(param);
try {
return Result.success(pointsMallReconciliationService.execute(param));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
@At
@@ -8,10 +8,6 @@ public class PointsMallReconciliationParam {
private String supplierId;
private Integer reconciliationType;
private String orderId;
private String queryOrderId;
private String userId;
private Integer reconciliationStatus;
private Integer invoiceStatus;
private String startDate;
private String endDate;
}
@@ -1,7 +1,6 @@
package com.budwk.app.zhgh.pointsmall.reconciliation.service;
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.zhgh.pointsmall.reconciliation.models.PointsMallReconciliationRecord;
import com.budwk.app.zhgh.pointsmall.reconciliation.param.PointsMallReconciliationOrderPageParam;
@@ -19,7 +18,7 @@ public interface PointsMallReconciliationService extends BaseService<PointsMallR
BigDecimal pointsTotal(PointsMallReconciliationParam param);
Result executeReconciliation(PointsMallReconciliationParam param);
String execute(PointsMallReconciliationParam param);
Pagination<Record> recordPage(PointsMallReconciliationRecordPageParam param);
@@ -2,66 +2,35 @@ package com.budwk.app.zhgh.pointsmall.reconciliation.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
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.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.reconciliation.models.PointsMallReconciliationOrder;
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.PointsMallReconciliationParam;
import com.budwk.app.zhgh.pointsmall.reconciliation.param.PointsMallReconciliationRecordPageParam;
import com.budwk.app.zhgh.pointsmall.reconciliation.service.PointsMallReconciliationService;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import lombok.extern.slf4j.Slf4j;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.entity.Record;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 对账功能
*/
@Slf4j
@IocBean(args = {"refer:dao"})
public class PointsMallReconciliationServiceImpl extends BaseServiceImpl<PointsMallReconciliationRecord> implements PointsMallReconciliationService {
@Inject
private MallReconciliationInfoQueryService mallReconciliationInfoQueryService;
@Inject
private MallUnReconciledMsgNoticeService unReconciledMsgNoticeService;
@Inject
private MallReconciliationResultNoticeService reconciliationResultNoticeService;
public PointsMallReconciliationServiceImpl(Dao dao) {
super(dao);
}
@@ -84,148 +53,37 @@ public class PointsMallReconciliationServiceImpl extends BaseServiceImpl<PointsM
return record == null || record.get("totalPoints") == null ? BigDecimal.ZERO : new BigDecimal(record.get("totalPoints").toString());
}
/**
* 执行对账
*
* @param reconciliationBO 入参
*/
@Override
public Result executeReconciliation(PointsMallReconciliationParam reconciliationBO) {
try {
checkReconciliationParam(reconciliationBO);
} catch (IllegalArgumentException e) {
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("部分勾选订单已不符合对账条件,请重新查询后选择");
public String execute(PointsMallReconciliationParam param) {
checkReconciliationParam(param);
List<PointsMallOrderSub> orders = dao().query(PointsMallOrderSub.class, reconciliationCnd(param, ""));
if (CollUtil.isEmpty(orders)) {
return "未查询到待对账订单";
}
QueryReconciliationInfoDTO queryReconciliationInfoDTO = new QueryReconciliationInfoDTO();
queryReconciliationInfoDTO.setSupplierId(reconciliationBO.getSupplierId());
queryReconciliationInfoDTO.setReconciliationType(reconciliationType);
queryReconciliationInfoDTO.setOrderId(String.join(",", orderIdList));
queryReconciliationInfoDTO.setStartDate(reconciliationBO.getStartDate());
queryReconciliationInfoDTO.setEndDate(reconciliationBO.getEndDate());
queryReconciliationInfoDTO.setPage(1);
queryReconciliationInfoDTO.setPageSize(100);
// 通过对账接口从供应商拉取对账订单数据
List<OrderInfoByReconciliationDTO> result = mallReconciliationInfoQueryService.queryReconciliationInfo(queryReconciliationInfoDTO);
if (CollUtil.isEmpty(result)) {
return Result.error("供应商侧没有拉取到对账订单数据");
}
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())));
String batchNo = DateUtil.format(new Date(), "yyyyMMddHHmmss") + IdUtil.fastSimpleUUID().substring(0, 6);
int success = 0;
int exception = 0;
List<PointsMallReconciliationRecord> records = new ArrayList<>();
// 根据供应商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);
}
List<PointsMallReconciliationRecord> reconciliationSuccessList = reconciliationList1.stream()
.filter(o -> o.getDiffType() == 0).collect(Collectors.toList());
if (CollUtil.isNotEmpty(reconciliationSuccessList)) {
// 更新子订单状态为对账完成
subOrderReconciliationUpdate(reconciliationSuccessList, 1);
for (PointsMallOrderSub order : orders) {
PointsMallReconciliationOrder supplierOrder = dao().fetch(PointsMallReconciliationOrder.class,
Cnd.where("orderId", "=", order.getOrderId())
.and("supplierId", "=", order.getSupplierId())
.and("delFlag", "=", false));
PointsMallReconciliationRecord record = buildRecord(order, supplierOrder, batchNo);
if (record.getDiffType() == null || record.getDiffType() == 0) {
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);
}
// 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();
dao().insert(records);
return "对账完成,批次号:" + batchNo + ",成功:" + success + ",异常:" + exception;
}
@Override
@@ -235,21 +93,21 @@ public class PointsMallReconciliationServiceImpl extends BaseServiceImpl<PointsM
cnd.and("r.supplierId", "=", param.getSupplierId());
}
if (StrUtil.isNotBlank(param.getOrderId())) {
cnd.and("r.orderId", "=", param.getOrderId());
cnd.and("r.orderId", "like", "%" + param.getOrderId() + "%");
}
if (StrUtil.isNotBlank(param.getUserId())) {
cnd.and("r.userId", "=", param.getUserId());
cnd.and("r.userId", "like", "%" + param.getUserId() + "%");
}
if (param.getDiffType() != null) {
cnd.and("r.diffType", "=", param.getDiffType());
}
if (StrUtil.isNotBlank(param.getBatchNo())) {
cnd.and("r.batchNo", "=", param.getBatchNo());
cnd.and("r.batchNo", "like", "%" + param.getBatchNo() + "%");
}
if (param.getNoticeStatus() != null) {
cnd.and("r.noticeStatus", "=", param.getNoticeStatus());
}
cnd.desc("r.batchNo").desc("r.createdAt");
cnd.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.setCondition(cnd);
return listPageMap(param.getPageNumber(), param.getPageSize(), sql);
@@ -261,8 +119,7 @@ public class PointsMallReconciliationServiceImpl extends BaseServiceImpl<PointsM
}
private Cnd orderCnd(PointsMallReconciliationOrderPageParam param, String alias) {
Cnd cnd = Cnd.where(field(alias, "delFlag"), "=", false)
.and(field(alias, "orderState"), "=", 5);
Cnd cnd = Cnd.where(field(alias, "delFlag"), "=", false);
if (StrUtil.isNotBlank(param.getSupplierId())) {
cnd.and(field(alias, "supplierId"), "=", param.getSupplierId());
}
@@ -288,23 +145,22 @@ public class PointsMallReconciliationServiceImpl extends BaseServiceImpl<PointsM
}
private Cnd reconciliationCnd(PointsMallReconciliationParam param, String alias) {
PointsMallReconciliationOrderPageParam query = new PointsMallReconciliationOrderPageParam();
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);
Cnd cnd = Cnd.where(field(alias, "delFlag"), "=", false);
cnd.and(field(alias, "supplierId"), "=", param.getSupplierId());
if (param.getReconciliationType() != null && param.getReconciliationType() == 2) {
cnd.and(field(alias, "reconciliationStatus"), "=", 2);
} else {
cnd.and(field(alias, "reconciliationStatus"), "=", 0);
}
if (StrUtil.isNotBlank(param.getOrderId())) {
cnd.and(field(alias, "orderId"), "in", StrUtil.splitTrim(param.getOrderId(), ","));
List<String> orderIds = 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;
}
@@ -334,233 +190,56 @@ public class PointsMallReconciliationServiceImpl extends BaseServiceImpl<PointsM
if (StrUtil.isBlank(param.getStartDate()) || StrUtil.isBlank(param.getEndDate())) {
throw new IllegalArgumentException("对账日期不能为空");
}
if (param.getReconciliationType() != 1 && param.getReconciliationType() != 2) {
throw new IllegalArgumentException("对账类型不正确");
}
}
private void subOrderReconciliationUpdate(List<PointsMallReconciliationRecord> reconciliationList, Integer reconciliationStatus) {
// 更新子订单状态为对账异常
reconciliationList.forEach(o -> dao().update(PointsMallOrderSub.class,
Chain.make("reconciliationStatus", reconciliationStatus),
Cnd.where("orderId", "=", o.getOrderId()).and("supplierId", "=", o.getSupplierId())));
}
private PointsMallReconciliationRecord buildRecord(PointsMallOrderSub order, PointsMallReconciliationOrder supplierOrder, String batchNo) {
PointsMallReconciliationRecord record = new PointsMallReconciliationRecord();
record.setSupplierId(order.getSupplierId());
record.setOrderId(order.getOrderId());
record.setUserId(order.getUserId());
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);
private UnReconciledMsgNoticeDTO getUnReconciledMsgNoticeDTO(PointsMallReconciliationParam reconciliationBO,
List<PointsMallReconciliationRecord> reconciliationList2) {
UnReconciledMsgNoticeDTO noticeDTO = new UnReconciledMsgNoticeDTO();
noticeDTO.setUniqueSeqNo("111");
noticeDTO.setToken("token");
noticeDTO.setSupplierId(reconciliationBO.getSupplierId());
noticeDTO.setStartDate(reconciliationBO.getStartDate());
noticeDTO.setEndDate(reconciliationBO.getEndDate());
PointsMallReconciliationRecord reconciliation = reconciliationList2.get(0);
noticeDTO.setUnReconciledType(reconciliation.getDiffType() + "");
noticeDTO.setUnReconciledMsg(reconciliation.getDiffReason());
return noticeDTO;
}
/**
* 积分商城已确认收货对账接口未返回
*
* @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;
}
/**
* 积分商城不存在此订单无法对账
*
* @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;
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;
}
// 2. 如果其中有一个为 null或者商品总数不一致直接返回 false
if (list1 == null || list2 == null || list1.size() != list2.size()) {
return false;
}
record.setSRefund(BigDecimal.ZERO);
record.setSTotalPrice(nullToZero(supplierOrder.getTotalPrice()));
record.setSCashAmount(BigDecimal.ZERO);
record.setSPointsPrice(nullToZero(supplierOrder.getTotalPrice()));
// 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;
}
List<String> reasons = new ArrayList<>();
if (!amountEquals(record.getPTotalPrice(), record.getSTotalPrice())) {
reasons.add("订单总金额不一致");
}
return true;
if (!amountEquals(record.getPPointsPrice(), record.getSPointsPrice())) {
reasons.add("积分金额不一致");
}
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 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;
private BigDecimal nullToZero(BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}
/**
* 订单积分对账数据赋值
*
* @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;
private boolean amountEquals(BigDecimal left, BigDecimal right) {
return nullToZero(left).compareTo(nullToZero(right)) == 0;
}
}
@@ -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.param.PointsMallReconciliationRecordPageParam;
import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import org.nutz.dao.entity.Record;
import java.util.List;
import java.util.Map;
public interface PointsMallReconciliationRecordService extends BaseService<PointsMallReconciliationRecord> {
Pagination<Map<String, Object>> page(PointsMallReconciliationRecordPageParam param);
Pagination<Record> page(PointsMallReconciliationRecordPageParam param);
List<PointsMallSupplier> listSuppliers();
}
@@ -3,7 +3,6 @@ package com.budwk.app.zhgh.pointsmall.reconciliationrecord.service.impl;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.Pagination;
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.param.PointsMallReconciliationRecordPageParam;
import com.budwk.app.zhgh.pointsmall.reconciliationrecord.service.PointsMallReconciliationRecordService;
@@ -11,14 +10,11 @@ import com.budwk.app.zhgh.pointsmall.supplier.models.PointsMallSupplier;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.entity.Record;
import org.nutz.dao.sql.Sql;
import org.nutz.ioc.loader.annotation.IocBean;
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"})
public class PointsMallReconciliationRecordServiceImpl extends BaseServiceImpl<PointsMallReconciliationRecord> implements PointsMallReconciliationRecordService {
@@ -28,46 +24,30 @@ public class PointsMallReconciliationRecordServiceImpl extends BaseServiceImpl<P
}
@Override
public Pagination<Map<String, Object>> page(PointsMallReconciliationRecordPageParam param) {
public Pagination<Record> page(PointsMallReconciliationRecordPageParam param) {
Cnd cnd = Cnd.where("r.delFlag", "=", false);
if (StrUtil.isNotBlank(param.getSupplierId())) {
cnd.and("r.supplierId", "=", param.getSupplierId());
}
if (StrUtil.isNotBlank(param.getOrderId())) {
cnd.and("r.orderId", "=", param.getOrderId());
cnd.and("r.orderId", "like", "%" + param.getOrderId() + "%");
}
if (StrUtil.isNotBlank(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()));
cnd.and("r.userId", "like", "%" + param.getUserId() + "%");
}
if (param.getDiffType() != null) {
cnd.and("r.diffType", "=", param.getDiffType());
}
if (StrUtil.isNotBlank(param.getBatchNo())) {
cnd.and("r.batchNo", "=", param.getBatchNo());
cnd.and("r.batchNo", "like", "%" + param.getBatchNo() + "%");
}
if (param.getNoticeStatus() != null) {
cnd.and("r.noticeStatus", "=", param.getNoticeStatus());
}
cnd.desc("r.batchNo").desc("r.createdAt");
cnd.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.setCondition(cnd);
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;
return listPageMap(param.getPageNumber(), param.getPageSize(), sql);
}
@Override
@@ -1,36 +1,23 @@
package com.budwk.app.zhgh.pointsmall.supplier.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.IdUtil;
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.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.param.PointsMallSupplierPageParam;
import com.budwk.app.zhgh.pointsmall.supplier.service.PointsMallSupplierService;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
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 java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@IocBean(args = {"refer:dao"})
public class PointsMallSupplierServiceImpl extends BaseServiceImpl<PointsMallSupplier> implements PointsMallSupplierService {
@Inject
private RedisService redisService;
public PointsMallSupplierServiceImpl(Dao dao) {
super(dao);
}
@@ -65,32 +52,7 @@ public class PointsMallSupplierServiceImpl extends BaseServiceImpl<PointsMallSup
cnd.and("mallType", "=", mallType);
}
cnd.asc("sortCode");
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);
return dao().query(PointsMallSupplier.class, cnd);
}
@Override
-7
View File
@@ -510,10 +510,3 @@ 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_status` (`status`)
) 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;
@@ -36,7 +36,7 @@ layout("/layouts/platform.html"){
新增场地
</el-button>
</table-tool>
<el-table :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table v-loading="tableLoading" :data="tableData" @sort-change="pageOrder" :size="tableSize">
<el-table-column label="序号" type="index" :index="indexMethod" width="60"></el-table-column>
<el-table-column
v-for="column in tableColumns"
@@ -152,11 +152,23 @@ layout("/layouts/platform.html"){
.catch(() => {})
},
switchChange(row) {
this.$axios.post("/platform/siteCug/manage/submit", row).then((res) => {
const previousState = !row.state
this.tableLoading = true
this.$axios.post("/platform/siteCug/manage/switchState", {
id: row.id,
state: row.state
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
} else {
// 状态更新失败时恢复开关,避免页面显示与数据库实际状态不一致。
this.$set(row, "state", previousState)
this.$message.warning(res.msg)
}
}).catch(() => {
this.$set(row, "state", previousState)
}).finally(() => {
this.tableLoading = false
})
},
querySiteType() {
@@ -109,15 +109,27 @@ layout("/layouts/platform.html"){
if (!value) return []
try {
const data = typeof value === "string" ? JSON.parse(value) : value
return Array.isArray(data) ? data : []
if (Array.isArray(data)) return data
if (data && typeof data === "object") {
return Object.keys(data).map(item => ({item, result: data[item]}))
}
return []
} catch (e) {
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) {
return this.parseVoteResult(value).map(item => {
return (item.item || item.itemName || "投票项") + "" +
(item.selected || item.option || item.value || "-")
this.voteOptionText(item)
}).join("")
},
buildSummary(records) {
@@ -125,7 +137,7 @@ layout("/layouts/platform.html"){
records.forEach(record => {
this.parseVoteResult(record.voteResult).forEach(item => {
const itemName = item.item || item.itemName || "投票项"
const option = item.selected || item.option || item.value || "-"
const option = this.voteOptionText(item)
if (!counter[itemName]) counter[itemName] = {}
counter[itemName][option] = (counter[itemName][option] || 0) + 1
})
@@ -110,10 +110,10 @@ const PROPOSAL_INFO = {
</el-dialog>
<!--审核信息支持按需同时展开多条记录初次加载和切换提案时仍保持全部折叠-->
<template v-if="doneTasks.length">
<template v-if="displayDoneTasks.length">
<div class="process-title">审核信息</div>
<el-collapse v-model="activeTaskIds">
<el-collapse-item v-for="task in doneTasks" :key="task.id" :name="task.id">
<el-collapse-item v-for="task in displayDoneTasks" :key="task.id" :name="task.id">
<template slot="title">
<!--折叠标题使用独立的单行布局避免复用 process-title 后产生外边距和高度冲突-->
<div style="display: flex;align-items: center;justify-content: space-between;width: 100%;min-width: 0;padding-left: 10px">
@@ -125,8 +125,28 @@ const PROPOSAL_INFO = {
</span>
</div>
</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"
v-if="task.ext.isFirstTaskNode">
v-else-if="task.ext.isFirstTaskNode">
<el-descriptions-item label="申请用户">{{ task.ext.initiatorName
}}({{task.ext.initiatorAccount}})
</el-descriptions-item>
@@ -233,6 +253,7 @@ const PROPOSAL_INFO = {
tableLoading: false,
viewData: {},
doneTasks: [],
displayDoneTasks: [],
activeTaskIds: [],
row: null,
viewDialogVisible: false
@@ -258,6 +279,7 @@ const PROPOSAL_INFO = {
this.$set(this, "tableLoading", true)
// 切换提案时先清空上一条提案的审核记录和展开状态,避免异步加载期间展示旧数据。
this.doneTasks = []
this.displayDoneTasks = []
this.activeTaskIds = []
let pendingRequestCount = 2
const handleRequestComplete = () => {
@@ -293,15 +315,94 @@ const PROPOSAL_INFO = {
return this.$axios.post("/flow/common/doneTasks", {bizId: this.row.id}).then((res) => {
if (res.code === 0) {
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.$emit("done-tasks", tasks)
this.$emit("done-tasks", this.doneTasks)
}
})
},
/**
* 按提案业务阶段对审核历史排序
*
* @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(){
this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId)
}
@@ -179,16 +179,17 @@ layout("/layouts/platform.html"){
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="tf_attachment" label="附件">
<file-upload
:value.sync="formData.tf_attachment"
upload_mode="drag"
:upload_number="5"
upload_result_category="array"
complete_result
upload_mode="file"
></file-upload>
</el-form-item>
<!-- 承办单位答复附件上传入口暂时停用,保留原配置便于后续恢复。 -->
<!-- <el-form-item prop="tf_attachment" label="附件">-->
<!-- <file-upload-->
<!-- :value.sync="formData.tf_attachment"-->
<!-- upload_mode="drag"-->
<!-- :upload_number="5"-->
<!-- upload_result_category="array"-->
<!-- complete_result-->
<!-- upload_mode="file"-->
<!-- ></file-upload>-->
<!-- </el-form-item>-->
<el-form-item label="答复内容" prop="tf_opinion"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<text-editor v-model="formData.tf_opinion"></text-editor>
@@ -14,11 +14,19 @@ layout("/layouts/platform.html"){
<el-option label="失败" :value="1"></el-option>
</el-select>
</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>
</el-card>
<el-card class="mt10" shadow="never">
<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>
<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>
@@ -28,11 +36,13 @@ layout("/layouts/platform.html"){
<el-tag :type="invoiceResultType(row.bSuccess)" size="mini">{{ invoiceResultName(row.bSuccess) }}</el-tag>
</template>
</el-table-column>
<el-table-column align="center" fixed="right" label="操作" width="340">
<el-table-column align="center" label="成功订单" min-width="220" prop="sucOrderIds" show-overflow-tooltip></el-table-column>
<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}">
<el-button @click="openOrderDialog(row.sucOrderIds)" size="mini" type="primary">开票成功列表</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>
<el-button @click="openInfoDialog(row)" size="mini" type="primary">发票明细</el-button>
<el-button @click="openOrderDialog(row.sucOrderIds)" size="mini">成功订单</el-button>
</template>
</el-table-column>
</el-table>
@@ -48,25 +58,120 @@ layout("/layouts/platform.html"){
></el-pagination>
</el-card>
<el-dialog :close-on-click-modal="false" title="订单明细" :visible.sync="orderDialogVisible" top="5vh" width="90%">
<el-table :data="orderList" border size="small" style="width: 100%" v-loading="orderLoading">
<el-dialog :close-on-click-modal="false" title="开票申请" :visible.sync="applyDialogVisible" top="5vh" width="720px">
<el-form :model="applyForm" label-width="120px" size="small">
<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="主订单号" 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="110">
<template slot-scope="{row}">
<el-tag :type="orderStatusType(row.orderState)" size="mini">{{ orderStateName(row.orderState) }}</el-tag>
</template>
</el-table-column>
<el-table-column align="center" label="票状态" min-width="100">
<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="160" prop="orderCompleteTime"></el-table-column>
<el-table-column align="center" label="订单总金额" min-width="110" prop="totalPrice"></el-table-column>
<el-table-column align="center" label="积分金额" min-width="110" prop="pointsPrice"></el-table-column>
<el-table-column align="center" label="退款积分" min-width="110" prop="refund"></el-table-column>
<el-table-column align="center" label="票状态" min-width="100">
<template slot-scope="{row}">
<el-tag :type="invoiceStatusType(row.invoiceStatus)" size="mini">{{ invoiceStatusName(row.invoiceStatus) }}</el-tag>
</template>
</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-pagination
:current-page="orderForm.pageNumber"
@@ -79,30 +184,37 @@ layout("/layouts/platform.html"){
style="margin-top: 16px; text-align: right"
></el-pagination>
<span slot="footer">
<span style="margin-right: 20px">总消耗积分:{{ orderTotalPoints }}</span>
<el-button @click="orderDialogVisible = false">关闭</el-button>
</span>
</el-dialog>
<el-dialog :close-on-click-modal="false" title="发票明细列表" :visible.sync="infoDialogVisible" top="5vh" width="90%">
<el-table :data="infoList" stripe size="small" style="width: 100%" v-loading="infoLoading">
<el-table-column label="发票主ID" min-width="160" prop="mainId" show-overflow-tooltip></el-table-column>
<el-table-column label="发票号码" min-width="170" prop="invoiceId" show-overflow-tooltip></el-table-column>
<el-table-column label="发票代码" min-width="160" prop="invoiceCode" show-overflow-tooltip></el-table-column>
<el-table-column label="发票日期" min-width="140" prop="invoiceDate"></el-table-column>
<el-table-column label="发票裸价" min-width="130" prop="invoiceNakeAmount"></el-table-column>
<el-table-column label="发票税率" min-width="110" prop="invoiceTaxRate"></el-table-column>
<el-table-column label="发票税额" min-width="130" prop="invoiceTaxAmount"></el-table-column>
<el-table-column label="价税合计" min-width="130" prop="invoiceAmount"></el-table-column>
<el-table-column label="发票类型" min-width="180">
<template slot-scope="{row}"><el-tag size="mini">{{ Number(row.invoiceType) === 9 ? '全电发票(增值税专用发票)' : Number(row.invoiceType) === 10 ? '全电发票(增值税普通发票)' : invoiceTypeName(row.invoiceType) }}</el-tag></template>
</el-table-column>
<el-table-column align="center" label="电子发票" width="110">
<template slot-scope="{row}"><el-button @click="previewInvoice(row)" size="mini" type="text">预览</el-button></template>
</el-table-column>
<el-table-column align="center" fixed="right" label="操作" width="100">
<template slot-scope="{row}"><el-button @click="openInvoiceDetail(row)" size="mini" type="text">详情</el-button></template>
<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-form-item label="发票号码">
<el-input clearable placeholder="发票号码" v-model="infoForm.invoiceId"></el-input>
</el-form-item>
<el-form-item label="发票代码">
<el-input clearable placeholder="发票代码" v-model="infoForm.invoiceCode"></el-input>
</el-form-item>
<el-form-item label="发票日期">
<el-date-picker clearable placeholder="发票日期" type="date" value-format="yyyy-MM-dd" v-model="infoForm.invoiceDate"></el-date-picker>
</el-form-item>
<el-form-item>
<el-button @click="infoSearch" type="primary">查询</el-button>
</el-form-item>
</el-form>
<el-table :data="infoList" border size="small" style="width: 100%" v-loading="infoLoading">
<el-table-column :index="infoIndexMethod" align="center" label="序号" type="index" width="70"></el-table-column>
<el-table-column align="center" label="发票号码" min-width="150" prop="invoiceId" show-overflow-tooltip></el-table-column>
<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 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-pagination
:current-page="infoForm.pageNumber"
@@ -113,46 +225,9 @@ layout("/layouts/platform.html"){
style="margin-top: 16px; text-align: right"
></el-pagination>
<span slot="footer">
<el-button @click="retryInvoice(infoForm.mainId)" :loading="retryLoading" type="primary">重新获取发票</el-button>
<el-button @click="infoDialogVisible = false">关闭</el-button>
</span>
</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>
<script nonce="${cspNonce!}">
@@ -161,6 +236,7 @@ layout("/layouts/platform.html"){
mixins: [initTableMixins],
data() {
return {
supplierList: [],
tableData: [],
tableLoading: false,
pageForm: {
@@ -168,28 +244,48 @@ layout("/layouts/platform.html"){
pageSize: 10,
totalCount: 0,
settlementId: null,
bSuccess: null
bSuccess: null,
sucOrderIds: null,
failOrderIds: null,
failMsg: null
},
applyDialogVisible: false,
applyLoading: false,
applyDateRange: [],
orderTotalPoints: 0,
applyForm: this.emptyApplyForm(),
orderDialogVisible: false,
orderLoading: false,
orderDateRange: [],
orderList: [],
orderSelections: [],
orderForm: this.emptyOrderForm(),
infoDialogVisible: false,
infoLoading: false,
infoList: [],
infoForm: this.emptyInfoForm(),
detailDialogVisible: false,
detailLoading: false,
retryLoading: false,
pdfDialogVisible: false,
pdfLoading: false,
pdfUrl: null,
detailData: {}
infoForm: this.emptyInfoForm()
}
},
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() {
return {
pageNumber: 1,
@@ -199,7 +295,7 @@ layout("/layouts/platform.html"){
orderId: null,
startDate: null,
endDate: null,
invoiceStatus: null
invoiceStatus: 0
}
},
emptyInfoForm() {
@@ -247,21 +343,79 @@ layout("/layouts/platform.html"){
this.pageForm.pageNumber = 1
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) {
this.orderDialogVisible = true
this.orderForm = this.emptyOrderForm()
this.orderDateRange = []
this.orderList = []
this.orderTotalPoints = 0
if (typeof orderIds !== "string" || !orderIds.trim()) {
return
if (typeof orderIds === "string" && orderIds) {
this.orderForm.orderId = orderIds
this.orderForm.invoiceStatus = null
}
this.orderForm.orderId = orderIds
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() {
if (!this.orderForm.orderId) return
this.syncOrderDateRange()
this.orderLoading = true
const resp = await this.$axios.post(loc() + "/orderPage", this.orderForm)
this.orderLoading = false
@@ -281,77 +435,27 @@ layout("/layouts/platform.html"){
this.orderForm.pageNumber = 1
this.orderPageData()
},
async loadOrderTotalPoints() {
const resp = await this.$axios.post(loc() + "/orderTotalPoints", this.orderForm)
if (resp.code === 0) this.orderTotalPoints = resp.data || 0
else this.notifyWarning(resp.msg)
orderSelectionChange(rows) {
this.orderSelections = rows || []
},
async openInvoiceDetail(row) {
this.detailData = {}
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("暂无可预览的发票文件")
useOrdersForApply() {
if (!this.orderSelections.length) {
this.notifyWarning("请先选择订单")
return
}
if ((row.fileType || "pdf").toLowerCase() !== "pdf") {
this.$commonUtil.previewFile({ suffix: row.fileType.toLowerCase(), downloadPath: path, name: row.invoiceId || "发票" })
return
}
if (this.pdfLoading) return
this.closePdfPreview()
this.pdfDialogVisible = true
this.pdfLoading = true
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
const first = this.orderSelections[0]
this.applyForm = this.emptyApplyForm()
this.applyForm.supplierId = first.supplierId
this.applyForm.orderIds = this.orderSelections.map((item) => item.orderId).join(",")
this.applyForm.invoiceDate = new Date().toISOString().slice(0, 10)
this.applyDateRange = [this.orderForm.startDate, this.orderForm.endDate].filter(Boolean)
this.orderDialogVisible = false
this.applyDialogVisible = true
this.loadOrderTotalPoints()
},
openInfoDialog(row) {
this.infoForm = this.emptyInfoForm()
this.infoForm.mainId = row.id
this.infoList = []
this.infoDialogVisible = true
this.infoPageData()
},
@@ -366,7 +470,6 @@ layout("/layouts/platform.html"){
if (resp.code === 0) {
this.infoList = resp.data.list || []
this.infoForm.totalCount = resp.data.totalCount || 0
this.pageData()
} else {
this.notifyWarning(resp.msg)
}
@@ -376,18 +479,10 @@ layout("/layouts/platform.html"){
this.infoPageData()
},
invoiceResultName(value) {
return value == null ? "开票中" : Number(value) === 0 ? "成功" : "失败"
return Number(value) === 0 ? "成功" : "失败"
},
invoiceResultType(value) {
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"
return Number(value) === 0 ? "success" : "danger"
},
invoiceStatusName(value) {
const map = { 0: "未开票", 1: "已开票", 2: "开票中" }
@@ -403,11 +498,8 @@ layout("/layouts/platform.html"){
}
},
created() {
this.loadSuppliers()
this.pageData()
},
beforeDestroy() {
this.pdfDialogVisible = false
this.closePdfPreview()
}
})
</script>
@@ -3,74 +3,44 @@ layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card :body-style="{ paddingBottom: '2px' }" shadow="never">
<el-form label-width="96px" @submit.native.prevent>
<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-option :key="item.supplierId" :label="item.supplierName" :value="item.supplierId" v-for="item in supplierList"></el-option>
</el-select>
</el-form-item>
</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.startDate"></el-date-picker>
</el-form-item>
</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-form-item>
</el-col>
<el-col :lg="4" :md="12" :xs="24">
<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-form-item>
</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-option :key="item.value" :label="item.label" :value="item.value" v-for="item in invoiceStatusOptions"></el-option>
</el-select>
</el-form-item>
</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 shadow="never">
<search @search="doSearch">
<search-item label="供应商">
<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-select>
</search-item>
<search-item label="订单ID">
<el-input clearable placeholder="多个订单ID用英文逗号分隔" v-model="pageForm.orderId"></el-input>
</search-item>
<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>
</search-item>
<search-item label="结束日期">
<el-date-picker clearable placeholder="结束日期" style="width: 100%" type="date" value-format="yyyy-MM-dd" v-model="pageForm.endDate"></el-date-picker>
</search-item>
<search-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>
</search-item>
<search-item label="发票状态">
<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-select>
</search-item>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool label="对账订单列表"></table-tool>
<el-table ref="tableRef" :data="tableData" @selection-change="selectionChange" border class="vi-table" size="small" style="width: 100%" v-loading="tableLoading">
<el-table-column align="center" type="selection" width="50"></el-table-column>
<table-tool label="对账订单列表">
<el-button @click="openReconcileDialog" size="mini" type="success">对账</el-button>
<el-button @click="openRecordDialog" size="mini" type="primary">对账记录</el-button>
</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 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>
@@ -117,43 +87,36 @@ layout("/layouts/platform.html"){
</el-card>
<el-dialog :close-on-click-modal="false" title="确认对账" :visible.sync="reconcileDialogVisible" width="480px">
<el-alert :closable="false" show-icon style="margin-bottom: 16px" :title="reconcileSelectionCount ? '本次仅对已勾选的 ' + reconcileSelectionCount + ' 笔订单执行对账。' : '您尚未勾选订单,本次将对当前查询条件下所有符合对账条件的订单执行对账(包含其他分页)。请确认对账范围。'" :type="reconcileSelectionCount ? 'info' : 'warning'"></el-alert>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="本次对账金额">{{ reconcilePointsTotal }}</el-descriptions-item>
</el-descriptions>
<el-alert :closable="false" show-icon style="margin-top: 16px" title="请记录当前对账积分金额,确认对账后该数值将不再展示。" type="warning"></el-alert>
<span slot="footer">
<el-button @click="reconcileDialogVisible = false">取消</el-button>
<el-button @click="executeReconciliation" :loading="executeLoading" type="primary">确认对账</el-button>
</span>
</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 label-width="100px" size="small">
<el-form-item label="供应商">
<el-select clearable filterable placeholder="请选择供应商" style="width: 100%" v-model="reconcileForm.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="发票抬头" prop="invoiceTitle" required>
<el-input clearable placeholder="请输入发票抬头" v-model="applyForm.invoiceTitle"></el-input>
<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="纳税人识别号" prop="taxIdNumber" required>
<el-input clearable placeholder="请输入纳税人识别号" v-model="applyForm.taxIdNumber"></el-input>
<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="开户地址" prop="invoiceAddress" required>
<el-input clearable placeholder="请输入发票地址" v-model="applyForm.invoiceAddress"></el-input>
<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="联系电话" prop="invoiceContact" required>
<el-input clearable maxlength="11" placeholder="请输入联系电话" v-model="applyForm.invoiceContact"></el-input>
<el-form-item label="订单ID">
<el-input clearable placeholder="多个订单ID用英文逗号分隔" v-model="reconcileForm.orderId"></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 label="积分合计">
<span>{{ reconcilePointsTotal }}</span>
</el-form-item>
</el-form>
<el-alert :closable="false" show-icon title="确认后订单将更新为已对账或对账异常,并生成对账记录。" type="warning"></el-alert>
<span slot="footer">
<el-button @click="applyDialogVisible = false">取消</el-button>
<el-button @click="submitApply" :loading="applyLoading" type="primary">确认开票</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>
</span>
</el-dialog>
@@ -246,10 +209,6 @@ layout("/layouts/platform.html"){
supplierList: [],
tableData: [],
tableLoading: false,
selections: [],
lastQuery: null,
reconcileSelectionCount: 0,
hasSearched: false,
pageForm: {
pageNumber: 1,
pageSize: 10,
@@ -259,7 +218,6 @@ layout("/layouts/platform.html"){
userId: null,
startDate: null,
endDate: null,
reconciliationType: null,
reconciliationStatus: null,
invoiceStatus: null
},
@@ -274,38 +232,6 @@ layout("/layouts/platform.html"){
startDate: 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,
recordLoading: false,
recordList: [],
@@ -353,41 +279,14 @@ layout("/layouts/platform.html"){
return index + 1 + (this.recordForm.pageNumber - 1) * this.recordForm.pageSize
},
doSearch() {
this.hasSearched = true
this.selections = []
this.pageForm.pageNumber = 1
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() {
this.tableLoading = true
const query = Object.assign({}, this.pageForm)
const resp = await this.$axios.post(loc() + "/pageData", query)
const resp = await this.$axios.post(loc() + "/pageData", this.pageForm)
this.tableLoading = false
if (resp.code === 0) {
this.lastQuery = query
this.selections = []
if (this.$refs.tableRef) this.$refs.tableRef.clearSelection()
this.tableData = resp.data.list || []
this.pageForm.totalCount = resp.data.totalCount || 0
} else {
@@ -409,134 +308,52 @@ layout("/layouts/platform.html"){
this.supplierList = resp.data || []
}
},
async openReconcileDialog() {
if (this.tableLoading || !this.lastQuery) {
this.notifyWarning("请先查询订单列表")
return
}
const query = this.lastQuery
this.reconcileSelectionCount = this.selections.length
openReconcileDialog() {
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
supplierId: this.pageForm.supplierId,
reconciliationType: this.pageForm.reconciliationStatus === 2 ? 2 : 1,
orderId: this.pageForm.orderId,
startDate: this.pageForm.startDate,
endDate: this.pageForm.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
this.reconcileDialogVisible = true
this.loadPointsTotal()
},
async loadPointsTotal() {
if (!this.reconciliationParamsComplete()) {
return false
if (!this.reconcileForm.supplierId || !this.reconcileForm.startDate || !this.reconcileForm.endDate || !this.reconcileForm.reconciliationType) {
this.notifyWarning("请先选择供应商、对账类型和对账日期")
return
}
this.pointsTotalLoading = true
try {
const resp = await this.$axios.post(loc() + "/pointsTotal", this.reconcileForm)
if (resp.code === 0) {
this.reconcilePointsTotal = resp.data || 0
return true
}
const resp = await this.$axios.post(loc() + "/pointsTotal", this.reconcileForm)
this.pointsTotalLoading = false
if (resp.code === 0) {
this.reconcilePointsTotal = resp.data || 0
} else {
this.notifyWarning(resp.msg)
return false
} finally {
this.pointsTotalLoading = false
}
},
async executeReconciliation() {
if (!this.reconciliationParamsComplete()) {
this.notifyWarning("请先选择供应商、对账类型和对账日期,并查询订单列表")
if (!this.reconcileForm.supplierId || !this.reconcileForm.startDate || !this.reconcileForm.endDate || !this.reconcileForm.reconciliationType) {
this.notifyWarning("请先选择供应商、对账类型和对账日期")
return
}
this.executeLoading = true
try {
const resp = await this.$axios.post(loc() + "/execute", this.reconcileForm)
if (resp.code === 0) {
this.notifySuccess(resp.data || "对账完成")
this.reconcileDialogVisible = false
this.pageData()
} else {
this.notifyWarning(resp.msg)
}
} finally {
this.executeLoading = false
const resp = await this.$axios.post(loc() + "/execute", this.reconcileForm)
this.executeLoading = false
if (resp.code === 0) {
this.notifySuccess(resp.data || "对账完成")
this.reconcileDialogVisible = false
this.pageData()
} else {
this.notifyWarning(resp.msg)
}
},
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() {
this.recordDialogVisible = true
this.recordForm.supplierId = this.pageForm.supplierId
this.recordForm.orderId = null
this.recordForm.orderId = this.pageForm.orderId
this.recordPageData()
},
recordSearch() {
@@ -13,6 +13,25 @@ layout("/layouts/platform.html"){
<search-item label="订单ID">
<el-input clearable placeholder="请输入订单ID" v-model="pageForm.orderId"></el-input>
</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>
</el-card>
@@ -20,10 +39,11 @@ layout("/layouts/platform.html"){
<table-tool label="对账记录"></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 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="供应商" 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="120" prop="loginname" 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="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="pPointsPrice"></el-table-column>
@@ -37,7 +57,18 @@ layout("/layouts/platform.html"){
<el-tag :type="diffTypeTag(row.diffType)" size="mini">{{ diffTypeName(row.diffType) }}</el-tag>
</template>
</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" 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-pagination
:current-page="pageForm.pageNumber"
@@ -56,7 +87,7 @@ layout("/layouts/platform.html"){
<el-descriptions-item label="批次号">{{ currentRecord.batchNo || "-" }}</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.loginname || "-" }}</el-descriptions-item>
<el-descriptions-item label="用户ID">{{ currentRecord.userId || "-" }}</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.pCashAmount || "0" }}</el-descriptions-item>
@@ -90,7 +121,11 @@ layout("/layouts/platform.html"){
pageSize: 10,
totalCount: 0,
supplierId: null,
orderId: null
orderId: null,
userId: null,
diffType: null,
batchNo: null,
noticeStatus: null
},
detailDialogVisible: false,
currentRecord: null
@@ -14,12 +14,11 @@ const welfareOption = {
<el-table-column align="center" header-align="center" label="系统默认选择" width="140">
<template slot-scope="{row}">
<el-radio
v-model="systemDefaultOption"
:label="row"
@change="setSystemDefault(row)">
<el-checkbox
:value="!!row.isSystemDefault"
@change="setSystemDefault(row, $event)">
&nbsp;
</el-radio>
</el-checkbox>
</template>
</el-table-column>
@@ -124,9 +123,10 @@ const welfareOption = {
},
// 每个福利项目只允许配置一个系统默认选项,保存时会随福利选项一并提交。
setSystemDefault(defaultOption) {
setSystemDefault(defaultOption, checked = true) {
this.systemDefaultOption = checked ? defaultOption : null;
this.welfareList.forEach((item) => {
this.$set(item, 'isSystemDefault', item === defaultOption);
this.$set(item, 'isSystemDefault', checked && item === defaultOption);
});
},
@@ -200,7 +200,7 @@ const welfareOption = {
transform: translate(-50%, -50%);
}
/deep/ .welfare-option .el-radio__label {
/deep/ .welfare-option .el-checkbox__label {
padding-left: 0;
}
@@ -397,6 +397,19 @@ layout("/layouts/platform_h5.html"){
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 {
display: flex;
align-items: center;
@@ -700,6 +713,12 @@ layout("/layouts/platform_h5.html"){
取消报名
</van-button>
</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>
@@ -731,6 +750,12 @@ layout("/layouts/platform_h5.html"){
<i class="fa fa-trash"></i>取消报名
</van-button>
</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>
@@ -768,6 +793,16 @@ layout("/layouts/platform_h5.html"){
}
},
components: {},
computed: {
canUnionApplyOnPc() {
return this.$auth.hasRoleOr([
"SYSADMIN",
"SCHOOL_UNION_ADMIN",
"BRANCH_UNION_CHAIRMAN",
"BRANCH_UNION_WENTI_SPORTS"
]) && this.$auth.hasPermission("activity.apply")
}
},
methods: {
toggleViewMode() {
this.viewMode = this.viewMode === "card" ? "list" : "card"
@@ -324,11 +324,37 @@ 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-value {
.brand-course-card__detail:nth-child(2) .brand-course-card__detail-value {
overflow: hidden;
text-overflow: ellipsis;
overflow-wrap: normal;
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 {
display: flex;
min-width: 0;
@@ -913,7 +939,17 @@ layout("/layouts/platform_h5.html"){
<van-icon name="friends-o"></van-icon>
<div class="brand-course-card__detail-copy">
<div class="brand-course-card__detail-label">联系人</div>
<div class="brand-course-card__detail-value">{{row.courseInstructor}}</div>
<van-popover
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 class="brand-course-card__detail">
@@ -1009,6 +1045,7 @@ layout("/layouts/platform_h5.html"){
activeAssort: null,
filterOpened: false,
viewMode: 'card',
contactPopoverIndex: -1,
weekdayCNMap: {0: '周日', 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六'},
}
@@ -1017,6 +1054,9 @@ layout("/layouts/platform_h5.html"){
toggleViewMode() {
this.viewMode = this.viewMode === 'card' ? 'list' : 'card'
},
setContactPopover(index, visible) {
this.contactPopoverIndex = visible ? index : -1
},
onAssortChange(name) {
this.pageForm.assortTypes = name ? JSON.stringify([name]) : []
this.doSearch()
@@ -444,7 +444,7 @@ layout("/layouts/platform_h5.html"){
<button type="button" class="works-mine-card__action" @click="onView(row)">
<van-icon name="eye-o"></van-icon>查看
</button>
<button type="button" class="works-mine-card__action is-primary" @click="onEdit(row)">
<button type="button" class="works-mine-card__action" @click="onEdit(row)">
<van-icon name="edit"></van-icon>编辑
</button>
<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="确认删除"
@cancel="closeDeleteConfirm" @confirm="confirmDelete"></van-dialog>
<apply-form ref="applyFormRef"></apply-form>
<apply-form ref="applyFormRef" @submit-success="handleEditSubmitSuccess"></apply-form>
<info ref="infoRef"></info>
</div>
@@ -541,6 +541,10 @@ layout("/layouts/platform_h5.html"){
}
this.$refs.applyFormRef.onOpenEdit(row)
},
// 编辑提交成功后留在我的作品页面,仅刷新当前列表数据。
handleEditSubmitSuccess() {
this.loadWorks(true)
},
onDelete(row) {
this.$set(this, 'pendingDeleteRow', row)
window.h5HistoryLayerManager.open('works-collection-mine-delete-confirm')
@@ -8,7 +8,7 @@ const INFO = {
<van-sticky>
<van-nav-bar title="作品详情" left-text="返回" left-arrow @click-left="closeInfo"></van-nav-bar>
</van-sticky>
<div class="works-detail-scroll">
<div ref="detailScrollRef" class="works-detail-scroll">
<section class="works-detail-card">
<h2 class="works-detail-card__title">基础信息</h2>
<van-cell-group :border="false">
@@ -154,6 +154,7 @@ const INFO = {
onOpen(row) {
this.$set(this, "viewData", Object.assign({}, row, {files: this.normalizeFiles(row.files)}))
window.h5HistoryLayerManager.open("works-collection-mine-info")
this.resetScrollPosition()
this.$axios.post("/platform/activity/worksCollection/common/findOne", {id: row.id}).then((res) => {
if (res.code === 0) {
const viewData = res.data || {}
@@ -162,6 +163,12 @@ const INFO = {
}
})
},
// 弹窗组件会被复用,每次打开后需清除上一次查看时保留的滚动位置。
resetScrollPosition() {
this.$nextTick(() => {
if (this.$refs.detailScrollRef) this.$refs.detailScrollRef.scrollTop = 0
})
},
normalizeFiles(files) {
if (Array.isArray(files)) return files
if (!files) return []
@@ -222,24 +222,30 @@ layout("/layouts/platform_h5.html"){
white-space: nowrap;
}
.works-read-like-tag {
.works-read-like-summary {
display: inline-flex;
min-height: 24px;
padding: 3px 9px;
align-items: center;
flex: none;
border: 0;
border-radius: 12px;
color: #397de0;
background-color: #edf5ff;
color: #7f8da1;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
box-sizing: border-box;
}
.works-read-like-tag .van-icon {
margin-right: 4px;
font-size: 14px;
/* 顶部仅展示累计点赞信息,人员图标与无底色文字用于区别底部可点击的点赞按钮。 */
.works-read-like-summary .van-icon {
display: inline-flex;
width: 20px;
height: 20px;
margin-right: 5px;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #ffffff;
background-color: #8795aa;
font-size: 12px;
}
.works-read-files-row {
@@ -441,8 +447,9 @@ layout("/layouts/platform_h5.html"){
<div class="works-read-card-title">{{row.name || '未命名作品'}}</div>
<div class="works-read-card-activity">{{row.activityName || '暂无活动信息'}}</div>
</div>
<div class="works-read-like-tag">
<van-icon name="good-job-o"></van-icon>{{row.num || 0}}
<div class="works-read-like-summary">
<van-icon name="friends"></van-icon>
<span>已有 {{row.num || 0}} 人点赞</span>
</div>
</div>
</template>
@@ -10,7 +10,7 @@ const applyForm = {
<van-nav-bar title="作品提交" left-text="返回" left-arrow @click-left="closeApply"></van-nav-bar>
</van-sticky>
<van-form ref="formRef" class="works-apply-form form-container">
<div class="works-apply-scroll">
<div ref="applyScrollRef" class="works-apply-scroll">
<section class="works-apply-section">
<h2 class="works-apply-section__title">基础信息</h2>
<van-cell-group :border="false">
@@ -356,6 +356,7 @@ const applyForm = {
showOptionPicker: false, optionPopupType: "", optionPopupTitle: "",
optionPopupOptions: [], pendingOptionValue: "",
submitConfirmVisible: false, fileAccept: null, chooseWorksType: {},
submitSuccessMode: "navigate",
historyLayerPageKey: "works-collection-upload-h5",
historyLayerUnsubscribe: null, ownsHistoryRegistration: false
}
@@ -384,16 +385,19 @@ const applyForm = {
},
onOpen(row) {
this.resetFormData()
this.$set(this, "submitSuccessMode", "navigate")
this.$set(this, "row", row)
this.$set(this.formData, "activityId", row.id)
this.activityChange(row.id).then(() => {
window.h5HistoryLayerManager.open("works-collection-apply")
this.resetScrollPosition()
})
},
onOpenEdit(row) {
const formData = clone(row)
formData.files = this.normalizeFiles(formData.files)
this.resetFormData()
this.$set(this, "submitSuccessMode", "close")
this.$set(this, "row", formData)
this.$set(this, "formData", formData)
const user = this.$store.state.user || {}
@@ -408,6 +412,13 @@ const applyForm = {
}).then(() => {
this.setSelectedWorksType(formData.worksId)
window.h5HistoryLayerManager.open("works-collection-apply")
this.resetScrollPosition()
})
},
// 表单弹窗会被复用,每次打开后需清除上一次编辑或提交时保留的滚动位置。
resetScrollPosition() {
this.$nextTick(() => {
if (this.$refs.applyScrollRef) this.$refs.applyScrollRef.scrollTop = 0
})
},
// 附件可能由列表 SQL 返回 JSON 字符串,也可能已被序列化为数组;统一转换为上传组件需要的数组。
@@ -505,6 +516,13 @@ const applyForm = {
}).then((res) => {
if (res.code === 0) {
this.$toast.success("提交成功")
if (this.submitSuccessMode === "close") {
// 我的作品页使用弹窗编辑,成功后只关闭当前表单并通知父页面刷新列表。
window.h5HistoryLayerManager.close("works-collection-apply", () => {
this.$emit("submit-success")
})
return
}
window.h5HistoryLayerManager.closeAllAndNavigate("/platform/activity/worksCollection/mine/h5")
}
}).finally(() => { this.$set(this, "formLoading", false) })
@@ -128,7 +128,7 @@ layout("/layouts/platform_h5.html"){
<van-field v-model="formData.tf_opinion" class="club-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" :rules="[{ required: true, message: '请填写审批意见' }]" maxlength="100" show-word-limit></van-field>
</van-form>
</section>
<div class="club-audit-actions"><van-button :loading="formLoading" type="danger" block @click="handleTaskAction(6)">退回</van-button><van-button :loading="formLoading" type="danger" block @click="handleTaskAction(2)">不同意</van-button><van-button :loading="formLoading" type="primary" block @click="handleTaskAction(1)">同意</van-button></div>
<div class="club-audit-actions"><van-button :loading="formLoading" type="primary" block @click="handleTaskAction(1)">同意</van-button><van-button :loading="formLoading" type="danger" block @click="handleTaskAction(2)">拒绝</van-button><van-button :loading="formLoading" type="danger" block @click="handleTaskAction(6)">退回</van-button></div>
</template>
</info>
<van-dialog :value="confirmVisible" title="提示" :message="confirmMessage" show-cancel-button :confirm-button-loading="formLoading" @confirm="confirmPendingAction" @cancel="closeConfirm"></van-dialog>
@@ -62,22 +62,48 @@ layout("/layouts/platform_h5.html"){
</template>
</table-list>
</main>
<info ref="infoRef" page-key="club-mineclub" :merge-apply-info="true"></info>
<van-popup v-model="clubDetailVisible" position="right" :style="{ width: '100%', height: '100%' }" get-container="#app" class="club-user-join-popup" :close-on-click-overlay="false">
<div class="club-user-join-detail">
<van-nav-bar title="社团详情" left-text="返回" left-arrow fixed @click-left="closeClubDetail"></van-nav-bar>
<div class="club-user-join-detail__scroll">
<main class="club-user-join-detail__content">
<section class="club-user-join-card club-user-join-info-card">
<h3 class="club-user-join-card__title">社团基础信息</h3>
<van-cell-group :border="false">
<van-cell title="社团名称">{{ clubDetail.clubName || '--' }}</van-cell>
<van-cell title="成立时间">{{ clubDetail.foundTime || '--' }}</van-cell>
<van-cell title="社团编码">{{ clubDetail.clubCode || '--' }}</van-cell>
<van-cell title="社团类型"><dict-tag :options="dict.type.CLUB_REGISTER_TYPE" :value="clubDetail.clubType"></dict-tag></van-cell>
<van-cell title="会长">{{ clubDetail.clubLeader || '--' }}</van-cell>
<van-cell title="秘书长">{{ clubDetail.clubSecretary || '--' }}</van-cell>
<van-cell title="当前人数">{{ clubDetail.currentPeopleNum || 0 }} 人</van-cell>
<van-cell title="会费标准">{{ clubDetail.due || '--' }}</van-cell>
<van-cell title="社团介绍" class="club-user-join-info-long-cell"><div v-if="hasClubContent(clubDetail.introduce)" v-html="clubDetail.introduce"></div><span v-else>暂无</span></van-cell>
<van-cell title="社团宗旨" class="club-user-join-info-long-cell"><div v-if="hasClubContent(clubDetail.purpose)" v-html="clubDetail.purpose"></div><span v-else>暂无</span></van-cell>
<van-cell title="社团章程" class="club-user-join-info-long-cell"><file-preview v-if="clubDetail.rulesFile && clubDetail.rulesFile.length" :files="clubDetail.rulesFile" complete_result></file-preview><span v-else>暂无</span></van-cell>
<van-cell title="年度活动计划" class="club-user-join-info-long-cell"><file-preview v-if="clubDetail.yearPlanFile && clubDetail.yearPlanFile.length" :files="clubDetail.yearPlanFile" complete_result></file-preview><span v-else>暂无</span></van-cell>
</van-cell-group>
</section>
</main>
</div>
</div>
</van-popup>
<van-dialog :value="confirmVisible" title="提示" :message="confirmMessage" show-cancel-button :confirm-button-loading="formLoading" @confirm="confirmExit" @cancel="closeConfirm"></van-dialog>
</div>
<script nonce="${cspNonce!}">
<!--#include('../common/clubUserJoin.js'){}#-->
new Vue({
el: "#app", store, components: {info: clubUserJoin},
data() { return {pageForm: {pageNumber: 1, pageSize: 10, totalCount: 0, clubName: ''}, listLoading: true, formLoading: false, confirmVisible: false, currentRow: null, historyLayerUnsubscribe: null} },
el: "#app", store, dicts: ['CLUB_REGISTER_TYPE'],
data() { return {pageForm: {pageNumber: 1, pageSize: 10, totalCount: 0, clubName: ''}, listLoading: true, formLoading: false, clubDetailVisible: false, clubDetail: {}, confirmVisible: false, currentRow: null, historyLayerUnsubscribe: null} },
computed: { confirmMessage() { return this.currentRow ? '您确定要申请退出“' + this.currentRow.clubName + '”吗?' : '您确定要申请退出该社团吗?'; } },
created() { const manager = window.h5HistoryLayerManager; if (manager) { manager.ensureRegistered('club-mineclub'); this.historyLayerUnsubscribe = manager.subscribe((layers) => { this.$set(this, 'confirmVisible', layers.includes('club-mineclub-confirm')); }); } },
created() { const manager = window.h5HistoryLayerManager; if (manager) { manager.ensureRegistered('club-mineclub'); this.historyLayerUnsubscribe = manager.subscribe((layers) => { this.$set(this, 'clubDetailVisible', layers.includes('club-mineclub-detail')); this.$set(this, 'confirmVisible', layers.includes('club-mineclub-confirm')); }); } },
beforeDestroy() { if (this.historyLayerUnsubscribe) { this.historyLayerUnsubscribe(); } const manager = window.h5HistoryLayerManager; if (manager) { manager.unregister('club-mineclub'); } },
methods: {
onNavBack() { const manager = window.h5HistoryLayerManager; if (manager && manager.stack.length) { manager.close(); return; } historyBack(); },
onLoadingChange(state) { this.$set(this, 'listLoading', state.loading); },
onView(row) { this.$refs.infoRef.onOpen(Object.assign({}, row, {id: row.applyId}), '社团申请详情'); },
onView(row) { this.$set(this, 'clubDetail', Object.assign({}, row || {})); const manager = window.h5HistoryLayerManager; if (manager) { manager.open('club-mineclub-detail'); return; } this.$set(this, 'clubDetailVisible', true); },
closeClubDetail(afterClose) { const manager = window.h5HistoryLayerManager; if (manager) { manager.close('club-mineclub-detail', afterClose); return; } this.$set(this, 'clubDetailVisible', false); if (afterClose) { afterClose(); } },
hasClubContent(value) { return value !== null && value !== undefined && String(value).replace(/<[^>]*>/g, '').replace(/&nbsp;/gi, '').trim().length > 0; },
onExit(row) { this.$set(this, 'currentRow', row); const manager = window.h5HistoryLayerManager; if (manager) { manager.open('club-mineclub-confirm'); return; } this.$set(this, 'confirmVisible', true); },
closeConfirm(afterClose) { const manager = window.h5HistoryLayerManager; if (manager) { manager.close('club-mineclub-confirm', afterClose); return; } this.$set(this, 'confirmVisible', false); if (afterClose) { afterClose(); } },
confirmExit() { const row = this.currentRow; this.closeConfirm(() => { this.$set(this, 'formLoading', true); this.$axios.post('/platform/club/join/apply/submit', {data: JSON.stringify(row), mode: false}).then((res) => { if (res.code === 0) { this.$toast(res.msg); const manager = window.h5HistoryLayerManager; if (manager) { manager.closeAll(() => { this.$pjaxReplace('/platform/club/join/mine/h5'); }); } else { this.$pjaxReplace('/platform/club/join/mine/h5'); } } }).finally(() => { this.$set(this, 'formLoading', false); }); }); },
@@ -805,8 +805,13 @@ layout("/layouts/platform_h5.html"){
</van-popup>
</template>
<van-field class="more-text" name="files" required
label="附件" :rules="[{ required: true, message: '请上传附件' }]">
</van-cell-group>
</section>
<section class="form-section">
<h2 class="form-section__title">附件信息</h2>
<van-cell-group :border="false">
<van-field class="more-text reimburse-upload-field" name="files" required
label="" :rules="[{ required: true, message: '请上传附件' }]">
<template #input>
<h5-file-upload
slot="input"
@@ -1003,7 +1008,7 @@ layout("/layouts/platform_h5.html"){
</div>
</van-form>
</div>
<van-popup v-model:show="invoiceDialogVisible" position="bottom" round :close-on-click-overlay="true" class="invoice-popup">
<van-popup v-model:show="invoiceDialogVisible" position="bottom" round :close-on-click-overlay="true" get-container="#app" class="invoice-popup">
<van-nav-bar :title="invoiceDialogMode === 'edit' ? '编辑发票明细' : '新增发票明细'"
left-text="取消"
left-arrow
@@ -1013,7 +1018,7 @@ layout("/layouts/platform_h5.html"){
<van-loading type="spinner" color="#1677ff" size="24px">发票识别中...</van-loading>
</div>
<van-cell-group class="invoice-popup-card">
<van-field class="more-text" label="发票文件" required>
<van-field class="more-text invoice-popup-file-field" label="发票文件" required>
<template #input>
<h5-file-upload
slot="input"
@@ -143,7 +143,7 @@ layout("/layouts/platform_h5.html"){
return isNaN(amount) ? value : amount.toFixed(2) + "元"
},
onView(row) {
this.$refs.unionReimburseInfoRef.onOpen(row, "报销统计详情")
this.$refs.unionReimburseInfoRef.onOpen(row, "报销详情")
},
onEdit(row) {
this.$pjaxReplace("/platform/unionReimburse/apply/h5?bizId=" + row.id + "&taskId=" + row.startTaskId)
@@ -258,6 +258,15 @@
background: #f7f9fd;
}
/* 附件上传区域与困难帮扶申请页保持一致,避免上传控件被文本域样式包裹。 */
.union-reimburse-form .form-section .reimburse-upload-field .van-field__control {
min-height: 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
}
.union-reimburse-form .payment-section,
.union-reimburse-form .invoice-section { padding: 0 0 14px; }
.union-reimburse-form .payment-toolbar,
@@ -388,6 +397,15 @@
border-radius: 10px;
background: #f7f9fd;
}
/* 发票文件只保留上传控件本身,移除报销文本域的边框、背景和内边距。 */
.union-reimburse-form .invoice-popup-card .more-text.invoice-popup-file-field .van-field__control {
min-height: 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
}
.union-reimburse-form .invoice-popup-tip {
margin: 10px 0 0;
padding: 9px 11px;
@@ -15,7 +15,7 @@ const UNION_REIMBURSE_INFO = {
</div>
<div class="union-info-row">
<div class="union-info-label">经办人</div>
<div class="union-info-value union-info-link">{{ viewData.userName }}</div>
<div class="union-info-value">{{ viewData.userName }}</div>
</div>
<div class="union-info-row">
<div class="union-info-label">联系方式</div>
@@ -186,7 +186,7 @@ const UNION_REIMBURSE_INFO = {
<div class="union-info-section-title">活动信息</div>
<div class="union-info-row">
<div class="union-info-label">活动名称</div>
<div class="union-info-value union-info-link">{{ viewData.activityName }}</div>
<div class="union-info-value">{{ viewData.activityName }}</div>
</div>
<div class="union-info-row">
<div class="union-info-label">活动人数</div>
@@ -346,17 +346,6 @@ const UNION_REIMBURSE_INFO = {
return this.viewData.paymentDetails || []
}
},
watch: {
visible(value) {
const manager = window.h5HistoryLayerManager
if (!manager) return
if (value) {
manager.open(this.historyLayerName)
return
}
manager.close(this.historyLayerName)
}
},
mounted() {
this.injectInfoStyle()
const manager = window.h5HistoryLayerManager
@@ -386,12 +375,19 @@ const UNION_REIMBURSE_INFO = {
onOpen(row, title) {
this.$set(this, "row", row)
this.$set(this, "detailTitle", title || "报销详情")
this.$set(this, "visible", true)
this.$set(this, "currentPaymentIndex", 0)
// 详情弹层的显示状态由全局历史栈统一同步,确保系统返回键与页面返回按钮关闭同一层。
const manager = window.h5HistoryLayerManager
if (manager && manager.open(this.historyLayerName)) {
this.getInfo()
this.getDoneTasks()
return
}
this.$set(this, "visible", true)
this.getInfo()
this.getDoneTasks()
},
// 由全局历史栈关闭详情,保证遮罩、关闭按钮和浏览器返回键遵循同一顺序
// 只关闭当前详情对应的历史层,避免误关闭审核确认弹框等上层业务弹框
onClose() {
const manager = window.h5HistoryLayerManager
if (manager && manager.close(this.historyLayerName)) return
@@ -144,7 +144,7 @@ layout("/layouts/platform_h5.html"){
return value && this.$moment(value).isValid() ? this.$moment(value).format("YYYY-MM-DD HH:mm:ss") : ""
},
onView(row) {
this.$refs.unionReimburseInfoRef.onOpen(row, "报销申请详情")
this.$refs.unionReimburseInfoRef.onOpen(row, "报销详情")
},
onEdit(row) {
this.$pjaxReplace('/platform/unionReimburse/apply/h5?taskId=' + (row.startTaskId || '') + "&bizId=" + row.id)
@@ -166,7 +166,7 @@ layout("/layouts/platform_h5.html"){
submitTaskAction() { const submitType = this.pendingSubmitType; this.closeSubmitConfirm(() => { this.$set(this, "formLoading", true); this.$axios.post("/platform/unionReimburse/review/reviewTask", {data: JSON.stringify(Object.assign({}, this.formData)), submitType: submitType}).then((res) => { if (res.code === 0) { this.$toast.success("提交成功"); this.doSearch(); this.$refs.unionReimburseInfoRef.onClose() } else { this.$toast.fail(res.msg) } }).finally(() => { this.$set(this, "formLoading", false) }) }) },
onView(row) {
this.$set(this, "showApprovalForm", false)
this.$refs.unionReimburseInfoRef.onOpen(row, "报销申请详情")
this.$refs.unionReimburseInfoRef.onOpen(row, "报销详情")
},
onAudit(row) {
this.$set(this, "showApprovalForm", true)
@@ -82,6 +82,22 @@
background-color: #ff976a;
}
/* 校工会预审核四个操作按钮与 PC 端 Element UI 的 primary、danger、info 配色保持一致。 */
.proposal-pre-audit-actions .proposal-pre-audit-button--primary {
border-color: var(--color-primary);
background-color: var(--color-primary);
}
.proposal-pre-audit-actions .proposal-pre-audit-button--danger {
border-color: #f56c6c;
background-color: #f56c6c;
}
.proposal-pre-audit-actions .proposal-pre-audit-button--info {
border-color: #909399;
background-color: #909399;
}
.proposal-soft-pagination .van-pagination__item {
color: var(--proposal-soft-primary);
background-color: #f8fbff;
@@ -280,11 +296,18 @@
.proposal-view-card .van-cell__title {
flex: 0 0 92px;
width: 92px;
color: #596779;
color: #354255;
font-size: 14px;
font-weight: 600;
line-height: 24px;
}
/* 纵向字段中 flex-basis 会转为标题高度,需要取消横向字段使用的 92px 固定占位。 */
.proposal-view-card .direction-column-field .van-cell__title {
flex: none;
width: auto;
}
.proposal-view-card .van-cell__value {
min-width: 0;
color: #4f5d70;
@@ -495,7 +518,7 @@
.proposal-view-table-wrap .el-table {
min-width: 560px;
font-size: 16px;
font-size: 14px;
}
.proposal-view-slot:empty {
@@ -19,7 +19,8 @@ const PROPOSAL_INFO = {
:value="viewData.caseFilingType"></dict-tag>
</van-cell>
<van-cell title="提案类型">{{ viewData.typeName || '--' }}</van-cell>
<van-cell title="提案方式">{{ getDictText(dict.type.PROPOSAL_SOURCE, viewData.source) }}</van-cell>
<van-cell title="提案方式">{{ getDictText(dict.type.PROPOSAL_SOURCE, viewData.source) }}
</van-cell>
<van-cell title="代表姓名">{{ viewData.createUserName || '--' }}</van-cell>
<van-cell title="所属教代会">{{ viewData.sessionName || viewData.fullName || '--' }}</van-cell>
<van-cell :title="viewData.committeeName ? '所属委员会' : '所属代表团'">
@@ -28,7 +29,7 @@ const PROPOSAL_INFO = {
<van-cell title="单位">{{ viewData.unitName || '--' }}</van-cell>
<van-cell title="联系电话">{{ viewData.mobile || '--' }}</van-cell>
<van-cell title="提案时间">{{ viewData.createTime || '--' }}</van-cell>
<van-cell title="提案案由" class="proposal-view-long-cell">
<van-cell title="案由" class="proposal-view-long-cell">
<div class="proposal-view-rich-value" v-html="viewData.brief || '--'"></div>
</van-cell>
<van-cell title="建议措施" class="proposal-view-long-cell">
@@ -62,7 +63,9 @@ const PROPOSAL_INFO = {
<h2 class="proposal-view-card__title">委员会成员意见 {{ index + 1 }}</h2>
<van-cell-group :border="false">
<van-cell title="委员">
{{ opinion.commissionerName || '--' }}<template v-if="opinion.commissionerLoginName">{{ opinion.commissionerLoginName }}</template>
{{ opinion.commissionerName || '--' }}
<template v-if="opinion.commissionerLoginName">{{ opinion.commissionerLoginName }}
</template>
</van-cell>
<van-cell title="立案结果">
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_RESULT"
@@ -109,12 +112,12 @@ const PROPOSAL_INFO = {
<!-- 点击时间轴节点后只在此处展示该任务原有的完整详情 -->
<section class="proposal-view-card proposal-flow-detail" v-if="selectedTask">
<h2 class="proposal-view-card__title">节点详情</h2>
<h3 class="proposal-flow-detail__name">{{ selectedTask.displayName || '--' }}</h3>
<h2 class="proposal-view-card__title">{{ selectedTask.displayName || '--' }}</h2>
<van-cell-group :border="false">
<template v-if="selectedTask.ext && selectedTask.ext.isFirstTaskNode">
<van-cell title="申请用户">
{{ selectedTask.ext.initiatorName || '--' }}{{ selectedTask.ext.initiatorAccount || '--' }}
{{ selectedTask.ext.initiatorName || '--' }}{{ selectedTask.ext.initiatorAccount ||
'--' }}
</van-cell>
<van-cell title="申请时间">{{ selectedTask.finishTime || '--' }}</van-cell>
<van-cell title="办理结果">
@@ -125,26 +128,35 @@ const PROPOSAL_INFO = {
<template v-else-if="selectedTask.taskName === 'invite'">
<van-cell title="办理用户">
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName || '--' }}
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName
|| '--' }}
</van-cell>
<van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell>
<van-cell title="附议人" class="proposal-view-long-cell">
<div class="proposal-view-table-wrap">
<el-table :data="selectedTask?.taskFormData?.seconder">
<el-table-column label="姓名" prop="userName"></el-table-column>
<el-table-column label="工号" prop="loginName"></el-table-column>
<el-table-column label="性别" prop="sex"></el-table-column>
<el-table-column label="单位" prop="unitName" show-overflow-tooltip></el-table-column>
<el-table-column label="分工会" prop="unionName" show-overflow-tooltip></el-table-column>
<el-table-column label="代表团" prop="delegationName" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="userName" width="80px"
header-align="center" align="center"></el-table-column>
<el-table-column label="工号" prop="loginName" width="80px"
header-align="center" align="center"></el-table-column>
<el-table-column label="性别" prop="sex" width="50px" header-align="center"
align="center"></el-table-column>
<el-table-column label="单位" prop="unitName"
show-overflow-tooltip header-align="center"
align="center"></el-table-column>
<el-table-column label="代表团" prop="delegationName"
show-overflow-tooltip header-align="center"
align="center"></el-table-column>
</el-table>
</div>
</van-cell>
</template>
<template v-else-if="selectedTask.taskName === 'committee' || selectedTask.taskName === 'committeeFilingUnit'">
<template
v-else-if="selectedTask.taskName === 'committee' || selectedTask.taskName === 'committeeFilingUnit'">
<van-cell title="办理用户">
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName || '--' }}
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName
|| '--' }}
</van-cell>
<van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell>
<van-cell title="立案结果">
@@ -155,19 +167,23 @@ const PROPOSAL_INFO = {
<dict-tag :options="dict.type.PROPOSAL_CASE_FILING_TYPE"
:value="selectedTask.ext.tf_caseFilingType"></dict-tag>
</van-cell>
<template v-if="['CONFIRM_FILING', 'SUGGESTION'].includes(selectedTask.ext.tf_caseFilingResult)">
<template
v-if="['CONFIRM_FILING', 'SUGGESTION'].includes(selectedTask.ext.tf_caseFilingResult)">
<van-cell v-if="selectedTask.ext.tf_caseFilingResult === 'SUGGESTION'" title="承办单位">
{{ selectedTask?.ext?.tf_masterUnitNameStr || selectedTask?.ext?.tf_slaveUnitNameStr || '--' }}
{{ selectedTask?.ext?.tf_masterUnitNameStr || selectedTask?.ext?.tf_slaveUnitNameStr
|| '--' }}
</van-cell>
<template v-else>
<van-cell title="主办单位">{{ selectedTask?.ext?.tf_masterUnitNameStr || '--' }}</van-cell>
<van-cell title="协办单位">{{ selectedTask?.ext?.tf_slaveUnitNameStr || '--' }}</van-cell>
<van-cell title="主办单位">{{ selectedTask?.ext?.tf_masterUnitNameStr || '--' }}
</van-cell>
<van-cell title="协办单位">{{ selectedTask?.ext?.tf_slaveUnitNameStr || '--' }}
</van-cell>
</template>
</template>
<van-cell title="办理意见"
v-if="selectedTask.taskFormData.tf_opinion"
class="proposal-view-long-cell">
<div class="proposal-view-rich-value" v-html="selectedTask.taskFormData.tf_opinion"></div>
v-if="selectedTask.taskFormData.tf_opinion">
<div class="proposal-view-rich-value"
v-html="selectedTask.taskFormData.tf_opinion"></div>
</van-cell>
</template>
@@ -177,7 +193,8 @@ const PROPOSAL_INFO = {
{{ selectedTask.ext.underTakeName || '--' }}
</van-cell>
<van-cell title="办理用户">
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName || '--' }}
{{ selectedTask.taskFormData.userName || '--' }}{{ selectedTask.taskFormData.loginName
|| '--' }}
</van-cell>
<van-cell title="办理时间">{{ selectedTask.finishTime || '--' }}</van-cell>
<van-cell title="办理评价" v-if="selectedTask.taskName === 'feedback'">
@@ -189,9 +206,9 @@ const PROPOSAL_INFO = {
:value="selectedTask.ext.submitType"></dict-tag>
</van-cell>
<van-cell title="办理意见"
v-if="selectedTask.taskFormData && selectedTask.taskFormData.tf_opinion"
class="proposal-view-long-cell">
<div class="proposal-view-rich-value" v-html="selectedTask.taskFormData.tf_opinion"></div>
v-if="selectedTask.taskFormData && selectedTask.taskFormData.tf_opinion">
<div class="proposal-view-rich-value"
v-html="selectedTask.taskFormData.tf_opinion"></div>
</van-cell>
</template>
</van-cell-group>
@@ -1,114 +1,389 @@
const PROPOSAL_TASK_POPUP = {
template: /*language=HTML*/ `
<van-popup v-model="visible" position="right" class="proposal-full-popup" :close-on-click-overlay="false" get-container="#app">
<van-popup v-model="visible" position="right" class="proposal-full-popup" :close-on-click-overlay="false"
get-container="#app">
<div class="proposal-full-popup__page">
<van-nav-bar @click-left="close" left-arrow left-text="返回" :title="title" placeholder fixed></van-nav-bar>
<van-nav-bar @click-left="close" left-arrow left-text="返回" :title="title" placeholder
fixed></van-nav-bar>
<div class="proposal-full-popup__scroll">
<proposal-info v-if="bizId" :biz-id="bizId" :visible="visible">
<div v-if="isApproval">
<van-form ref="formRef">
<template v-if="kind === 'delegation' || kind === 'preAudit' || kind === 'schoolLeaderApproval'">
<template
v-if="kind === 'delegation' || kind === 'preAudit' || kind === 'schoolLeaderApproval'">
<section class="proposal-view-card">
<h2 class="proposal-view-card__title">审批意见<span class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" :rules="[{required:true,message:'请填写审批意见'}]" required maxlength="100" show-word-limit></van-field>
<h2 class="proposal-view-card__title">审批意见<span
class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field"
name="tf_opinion" type="textarea" rows="6" label=""
placeholder="请输入审批意见"
:rules="[{required:true,message:'请填写审批意见'}]" required
maxlength="100" show-word-limit></van-field>
</section>
</template>
<template v-if="kind === 'feedbackEvaluation'">
<van-field v-model="formData.tf_feedback_name" label="满意度" placeholder="请选择满意度" :rules="[{required:true,message:'请选择满意度'}]" required readonly is-link @click="openLayer('feedback-picker')"></van-field>
<section class="proposal-view-card">
<h2 class="proposal-view-card__title">审批意见<span class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" :rules="[{required:true,message:'请填写审批意见'}]" required maxlength="100" show-word-limit></van-field>
<h2 class="proposal-view-card__title">反馈评价<span
class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_feedback_name" label="满意度"
placeholder="请选择满意度"
:rules="[{required:true,message:'请选择满意度'}]" required readonly
is-link @click="openLayer('feedback-picker')"></van-field>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field"
name="tf_opinion" type="textarea" rows="6" label=""
placeholder="请输入反馈意见"
:rules="[{required:true,message:'请填写反馈意见'}]"
maxlength="100" show-word-limit></van-field>
</section>
</template>
<template v-if="kind === 'suggestion'">
<van-field v-model="formData.result" label="承办意向" placeholder="请选择承办意向" :rules="[{required:true,message:'请选择承办意向'}]" required readonly is-link @click="openLayer('result-picker')"></van-field>
<section class="proposal-view-card">
<h2 class="proposal-view-card__title">反馈意见<span class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.opinion" class="proposal-audit-field" name="opinion" type="textarea" rows="6" label="" placeholder="请输入反馈意见" :rules="[{required:true,message:'请输入反馈意见'}]" required show-word-limit></van-field>
<van-field v-model="formData.result" label="承办意向"
placeholder="请选择承办意向"
:rules="[{required:true,message:'请选择承办意向'}]" required readonly
is-link @click="openLayer('result-picker')"></van-field>
<h2 class="proposal-view-card__title">反馈意见<span
class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.opinion" class="proposal-audit-field"
name="opinion" type="textarea" rows="6" label=""
placeholder="请输入反馈意见"
:rules="[{required:true,message:'请输入反馈意见'}]" required
show-word-limit></van-field>
</section>
</template>
<template v-if="kind === 'unitReply'">
<section class="proposal-view-card">
<van-field :value="formData.underTakeName" label="承办单位" readonly></van-field>
<van-field v-model="formData.tf_implementStateName" label="落实情况" placeholder="请选择落实情况" :rules="[{required:true,message:'请选择落实情况'}]" required readonly is-link @click="openLayer('implement-picker')"></van-field>
<h2 class="proposal-view-card__title">答复内容<span class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入答复内容" :rules="[{required:true,message:'请填写答复内容'}]" required maxlength="100" show-word-limit></van-field>
<h2 class="proposal-view-card__title">承办单位答复<span
class="proposal-audit-card__required">*</span></h2>
<van-field :value="formData.underTakeName" label="承办单位"
readonly></van-field>
<van-field v-model="formData.tf_implementStateName" label="落实情况"
placeholder="请选择落实情况"
:rules="[{required:true,message:'请选择落实情况'}]" required readonly
is-link @click="openLayer('implement-picker')"></van-field>
<!-- 承办单位答复附件上传入口暂时停用保留公用上传组件配置便于后续恢复 -->
<!-- <van-field label="附件" name="tf_attachment" class="direction-column-field">-->
<!-- <template #input>-->
<!-- <h5-file-upload-->
<!-- :value.sync="formData.tf_attachment"-->
<!-- :upload_number="5"-->
<!-- upload_mode="file"-->
<!-- upload_result_category="array"-->
<!-- upload_result_type="url"-->
<!-- complete_result>-->
<!-- </h5-file-upload>-->
<!-- </template>-->
<!-- </van-field>-->
<van-field v-model="formData.tf_opinion" class="proposal-audit-field"
name="tf_opinion" type="textarea" rows="6" label=""
placeholder="请输入答复内容"
:rules="[{required:true,message:'请填写答复内容'}]"
maxlength="100" show-word-limit></van-field>
</section>
<van-field v-if="supportCandidate && formData.underTakeIsMaster" v-model="formData.tf_nextNodeOperatorName" label="校领导" placeholder="请选择校领导" :rules="[{required:true,message:'请选择校领导'}]" required readonly is-link @click="openLayer('candidate-picker')"></van-field>
<van-field v-if="supportCandidate && formData.underTakeIsMaster"
v-model="formData.tf_nextNodeOperatorName" label="校领导"
placeholder="请选择校领导"
:rules="[{required:true,message:'请选择校领导'}]" required readonly
is-link @click="openLayer('candidate-picker')"></van-field>
</template>
<template v-if="kind === 'caseCheck'">
<van-field :value="formData.tf_username" label="审核人" readonly></van-field>
<van-field :value="formData.tf_auditTime" label="审核时间" readonly></van-field>
<van-field :value="formData.tf_caseCheckNeedSecondReply" label="二次答复" required :rules="[{validator:validateSecondReply,message:'请选择是否需要二次答复'}]">
<template #input><van-radio-group v-model="formData.tf_caseCheckNeedSecondReply" direction="horizontal" @change="onSecondReplyChange"><van-radio :name="0">无需二次答复</van-radio><van-radio :name="1"></van-radio></van-radio-group></template>
<van-field :value="formData.tf_caseCheckNeedSecondReply" label="二次答复" required
:rules="[{validator:validateSecondReply,message:'请选择是否需要二次答复'}]">
<template #input>
<van-radio-group v-model="formData.tf_caseCheckNeedSecondReply"
direction="horizontal" @change="onSecondReplyChange">
<van-radio :name="0">无需二次答复</van-radio>
<van-radio :name="1">需要二次答复</van-radio>
</van-radio-group>
</template>
</van-field>
<van-field v-if="formData.tf_caseCheckNeedSecondReply === 1" :value="selectedReplyUnitNames" label="答复单位" placeholder="请选择办理单位" readonly required is-link :rules="[{validator:validateSecondReplyUnits,message:'请选择需要二次答复的办理单位'}]" @click="openReplyUnitPicker"></van-field>
<van-field v-if="formData.tf_caseCheckNeedSecondReply === 1"
:value="selectedReplyUnitNames" label="答复单位"
placeholder="请选择办理单位" readonly required is-link
:rules="[{validator:validateSecondReplyUnits,message:'请选择需要二次答复的办理单位'}]"
@click="openReplyUnitPicker"></van-field>
<section class="proposal-view-card">
<h2 class="proposal-view-card__title">审批意见<span class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field" name="tf_opinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" required maxlength="100" show-word-limit :rules="[{required:true,message:'请填写审批意见'}]"></van-field>
<h2 class="proposal-view-card__title">审批意见<span
class="proposal-audit-card__required">*</span></h2>
<van-field v-model="formData.tf_opinion" class="proposal-audit-field"
name="tf_opinion" type="textarea" rows="6" label=""
placeholder="请输入审批意见" required maxlength="100" show-word-limit
:rules="[{required:true,message:'请填写审批意见'}]"></van-field>
</section>
</template>
</van-form>
<div class="proposal-popup-form-actions" v-if="kind === 'delegation'">
<van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="prepareSubmit(6)">退回</van-button><van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="prepareSubmit(2)"></van-button><van-button type="primary" block :loading="formLoading" class="proposal-soft-button" @click="prepareSubmit(1)"></van-button>
<van-button type="primary" block :loading="formLoading" class="proposal-soft-button"
@click="prepareSubmit(1)">同意
</van-button>
<van-button type="danger" block :loading="formLoading"
class="proposal-soft-button proposal-soft-button--danger"
@click="prepareSubmit(2)">不同意
</van-button>
<van-button type="danger" block :loading="formLoading"
class="proposal-soft-button proposal-soft-button--danger"
@click="prepareSubmit(6)">退回
</van-button>
</div>
<div class="proposal-popup-form-actions" v-else-if="kind === 'preAudit'" style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr))">
<van-button type="warning" :loading="formLoading" class="proposal-soft-button proposal-soft-button--warning" @click="prepareSubmit(4)">退回不审核</van-button><van-button type="warning" :loading="formLoading" class="proposal-soft-button proposal-soft-button--warning" @click="prepareSubmit(6)">退</van-button><van-button type="danger" :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="prepareSubmit(2)"></van-button><van-button type="primary" :loading="formLoading" class="proposal-soft-button" @click="prepareSubmit(1)"></van-button>
<div class="proposal-popup-form-actions proposal-pre-audit-actions" v-else-if="kind === 'preAudit'"
style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr))">
<van-button type="primary" :loading="formLoading"
class="proposal-soft-button proposal-pre-audit-button--primary"
@click="prepareSubmit(1)">同意
</van-button>
<van-button type="danger" :loading="formLoading"
class="proposal-soft-button proposal-pre-audit-button--danger"
@click="prepareSubmit(2)">不同意
</van-button>
<van-button type="info" :loading="formLoading"
class="proposal-soft-button proposal-pre-audit-button--info"
@click="prepareSubmit(4)">退回不审核
</van-button>
<van-button type="info" :loading="formLoading"
class="proposal-soft-button proposal-pre-audit-button--info"
@click="prepareSubmit(6)">退回审核
</van-button>
</div>
<div class="proposal-popup-form-actions" v-else-if="kind === 'schoolLeaderApproval'">
<van-button type="primary" block :loading="formLoading" class="proposal-soft-button"
@click="prepareSubmit('agree')">同意答复
</van-button>
<van-button type="danger" block :loading="formLoading"
class="proposal-soft-button proposal-soft-button--danger"
@click="prepareSubmit('back')">退回答复
</van-button>
</div>
<div class="proposal-popup-form-actions" v-else>
<van-button type="primary" block :loading="formLoading" class="proposal-soft-button"
@click="prepareSubmit(1)">提交
</van-button>
<van-button block :disabled="formLoading" class="proposal-rounded-button"
@click="close">返回
</van-button>
</div>
<div class="proposal-popup-form-actions" v-else-if="kind === 'schoolLeaderApproval'"><van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="prepareSubmit('back')">退回答复</van-button><van-button type="primary" block :loading="formLoading" class="proposal-soft-button" @click="prepareSubmit('agree')"></van-button></div>
<div class="proposal-popup-form-actions" v-else><van-button block :disabled="formLoading" class="proposal-rounded-button" @click="close">取消</van-button><van-button type="primary" block :loading="formLoading" class="proposal-soft-button" @click="prepareSubmit(1)"></van-button></div>
</div>
</proposal-info>
</div>
</div>
<!-- 字典通过异步请求加载初始化阶段可能为 null使用空数组保证弹层首次渲染不执行 null.map -->
<van-popup v-model="feedbackPickerVisible" position="bottom" :close-on-click-overlay="false"><van-picker show-toolbar :columns="(dict.type.PROPOSAL_FEEDBACK || []).map((item) => ({text:item.label,value:item.code}))" @confirm="confirmFeedback" @cancel="closeLayer('feedback-picker')"></van-picker></van-popup>
<van-popup v-model="resultPickerVisible" position="bottom" :close-on-click-overlay="false"><van-picker show-toolbar :columns="(dict.type.UNDERTAKE_SUGGESTION || []).map((item) => item.code)" @confirm="confirmResult" @cancel="closeLayer('result-picker')"></van-picker></van-popup>
<van-popup v-model="implementPickerVisible" position="bottom" :close-on-click-overlay="false"><van-picker show-toolbar :columns="(dict.type.PROPOSAL_REPLY_IMPLEMENT || []).map((item) => ({text:item.label,value:item.code}))" @confirm="confirmImplement" @cancel="closeLayer('implement-picker')"></van-picker></van-popup>
<van-popup v-model="candidatePickerVisible" position="bottom" :close-on-click-overlay="false"><van-picker show-toolbar :columns="candidates.map((item) => ({text:item.userName,value:item.userId}))" @confirm="confirmCandidate" @cancel="closeLayer('candidate-picker')"></van-picker></van-popup>
<van-popup v-model="replyUnitPickerVisible" position="bottom" round :close-on-click-overlay="false" :style="{height:'70%'}">
<div class="reply-unit-popup"><div class="reply-unit-toolbar"><button type="button" @click="closeLayer('reply-unit-picker')">取消</button><div class="reply-unit-toolbar-title"></div><button type="button" @click="confirmReplyUnits"></button></div>
<div class="reply-unit-list" v-if="replyUnitOptions.length"><van-checkbox-group v-model="pendingReplyUnitIds"><van-cell v-for="item in replyUnitOptions" :key="item.unitId" clickable :title="item.unitName" :label="item.isMaster ? '主办单位' : '协办单位'" @click="toggleReplyUnit(item)"><template #right-icon><van-checkbox :name="item.unitId" @click.stop></van-checkbox></template></van-cell></van-checkbox-group></div>
<van-popup v-model="feedbackPickerVisible" position="bottom" :close-on-click-overlay="false">
<van-picker show-toolbar
:columns="(dict.type.PROPOSAL_FEEDBACK || []).map((item) => ({text:item.label,value:item.code}))"
@confirm="confirmFeedback" @cancel="closeLayer('feedback-picker')"></van-picker>
</van-popup>
<van-popup v-model="resultPickerVisible" position="bottom" :close-on-click-overlay="false">
<van-picker show-toolbar :columns="(dict.type.UNDERTAKE_SUGGESTION || []).map((item) => item.code)"
@confirm="confirmResult" @cancel="closeLayer('result-picker')"></van-picker>
</van-popup>
<van-popup v-model="implementPickerVisible" position="bottom" :close-on-click-overlay="false">
<van-picker show-toolbar
:columns="(dict.type.PROPOSAL_REPLY_IMPLEMENT || []).map((item) => ({text:item.label,value:item.code}))"
@confirm="confirmImplement" @cancel="closeLayer('implement-picker')"></van-picker>
</van-popup>
<van-popup v-model="candidatePickerVisible" position="bottom" :close-on-click-overlay="false">
<van-picker show-toolbar :columns="candidates.map((item) => ({text:item.userName,value:item.userId}))"
@confirm="confirmCandidate" @cancel="closeLayer('candidate-picker')"></van-picker>
</van-popup>
<van-popup v-model="replyUnitPickerVisible" position="bottom" round :close-on-click-overlay="false"
:style="{height:'70%'}">
<div class="reply-unit-popup">
<div class="reply-unit-toolbar">
<button type="button" @click="closeLayer('reply-unit-picker')">取消</button>
<div class="reply-unit-toolbar-title">选择答复单位</div>
<button type="button" @click="confirmReplyUnits">确定</button>
</div>
<div class="reply-unit-list" v-if="replyUnitOptions.length">
<van-checkbox-group v-model="pendingReplyUnitIds">
<van-cell v-for="item in replyUnitOptions" :key="item.unitId" clickable
:title="item.unitName" :label="item.isMaster ? '主办单位' : '协办单位'"
@click="toggleReplyUnit(item)">
<template #right-icon>
<van-checkbox :name="item.unitId" @click.stop></van-checkbox>
</template>
</van-cell>
</van-checkbox-group>
</div>
<div class="reply-unit-empty" v-else>暂无可选办理单位</div>
</div>
</van-popup>
<van-dialog :value="confirmVisible" title="提示" message="您确定要提交吗?" show-cancel-button @confirm="submit" @cancel="closeLayer('submit-confirm')"></van-dialog>
<van-dialog :value="confirmVisible" title="提示" message="您确定要提交吗?" show-cancel-button
@confirm="submit" @cancel="closeLayer('submit-confirm')"></van-dialog>
</van-popup>
`,
dicts: ["PROPOSAL_FEEDBACK", "UNDERTAKE_SUGGESTION", "PROPOSAL_REPLY_IMPLEMENT"],
components: {"proposal-info": PROPOSAL_INFO},
props: {kind: {type: String, required: true}, pageKey: {type: String, required: true}},
data() { return {visible:false,bizId:"",action:"view",formData:{},formLoading:false,pendingSubmitType:1,feedbackPickerVisible:false,resultPickerVisible:false,implementPickerVisible:false,candidatePickerVisible:false,replyUnitPickerVisible:false,confirmVisible:false,supportCandidate:false,candidates:[],replyUnitOptions:[],pendingReplyUnitIds:[],unsubscribeHistoryLayer:null} },
data() {
return {
visible: false,
bizId: "",
action: "view",
formData: {},
formLoading: false,
pendingSubmitType: 1,
feedbackPickerVisible: false,
resultPickerVisible: false,
implementPickerVisible: false,
candidatePickerVisible: false,
replyUnitPickerVisible: false,
confirmVisible: false,
supportCandidate: false,
candidates: [],
replyUnitOptions: [],
pendingReplyUnitIds: [],
unsubscribeHistoryLayer: null
}
},
computed: {
isApproval() { return this.action === "approval" },
title() { const titles={delegation:"团长审核",preAudit:"校工会预审核",schoolLeaderApproval:"分管校领导审批",feedbackEvaluation:"反馈评价",suggestion:"承办单位意见",unitReply:"承办单位答复",caseCheck:"提案委员会审查"}; return this.isApproval ? titles[this.kind] : "提案详情" },
selectedReplyUnitNames() { const ids=this.formData.tf_secondReplyUnitIds || []; return this.replyUnitOptions.filter((item) => ids.indexOf(item.unitId)>-1).map((item) => item.unitName+""+(item.isMaster?"主办":"协办")+"").join("、") }
isApproval() {
return this.action === "approval"
},
title() {
const titles = {
delegation: "团长审核",
preAudit: "校工会预审核",
schoolLeaderApproval: "分管校领导审批",
feedbackEvaluation: "反馈评价",
suggestion: "承办单位意见",
unitReply: "承办单位答复",
caseCheck: "提案委员会审查"
};
return this.isApproval ? titles[this.kind] : "提案详情"
},
selectedReplyUnitNames() {
const ids = this.formData.tf_secondReplyUnitIds || [];
return this.replyUnitOptions.filter((item) => ids.indexOf(item.unitId) > -1).map((item) => item.unitName + "" + (item.isMaster ? "主办" : "协办") + "").join("、")
}
},
methods: {
syncLayers(stack) { this.$set(this,"visible",stack.includes("detail")); this.$set(this,"feedbackPickerVisible",stack.includes("feedback-picker")); this.$set(this,"resultPickerVisible",stack.includes("result-picker")); this.$set(this,"implementPickerVisible",stack.includes("implement-picker")); this.$set(this,"candidatePickerVisible",stack.includes("candidate-picker")); this.$set(this,"replyUnitPickerVisible",stack.includes("reply-unit-picker")); this.$set(this,"confirmVisible",stack.includes("submit-confirm")) },
openLayer(name) { window.h5HistoryLayerManager.open(name) }, closeLayer(name,callback) { window.h5HistoryLayerManager.close(name,callback) }, close() { this.closeLayer("detail") },
syncLayers(stack) {
this.$set(this, "visible", stack.includes("detail"));
this.$set(this, "feedbackPickerVisible", stack.includes("feedback-picker"));
this.$set(this, "resultPickerVisible", stack.includes("result-picker"));
this.$set(this, "implementPickerVisible", stack.includes("implement-picker"));
this.$set(this, "candidatePickerVisible", stack.includes("candidate-picker"));
this.$set(this, "replyUnitPickerVisible", stack.includes("reply-unit-picker"));
this.$set(this, "confirmVisible", stack.includes("submit-confirm"))
},
openLayer(name) {
window.h5HistoryLayerManager.open(name)
}, closeLayer(name, callback) {
window.h5HistoryLayerManager.close(name, callback)
}, close() {
this.closeLayer("detail")
},
/** row 提供详情主键和当前 WF 任务参数,action 为 view 或 approval。 */
open(row,action) {
const defaultTaskNames={delegation:"团长审核",preAudit:"校工会预审核",feedbackEvaluation:"反馈评价",unitReply:"承办单位答复",caseCheck:"提案委员会审查"}
const taskName=this.kind === "unitReply" ? (row.taskName || defaultTaskNames[this.kind]) : (row.curTaskName || row.taskName || defaultTaskNames[this.kind] || "")
this.$set(this,"bizId",row.id); this.$set(this,"action",action); this.$set(this,"formData",{proposalId:row.id,processTaskId:row.taskId || "",taskKey:row.taskKey || "",taskName:taskName,instanceId:row.instanceId || "",underTakeName:row.underTakeName || "",underTakeIsMaster:!!row.underTakeIsMaster,tf_opinion:"",opinion:""})
if (action === "approval" && this.kind === "unitReply") this.loadCandidates(row.taskId || "")
if (action === "approval" && this.kind === "caseCheck") { this.$set(this.formData,"tf_auditTime",moment().format("YYYY-MM-DD")); this.$set(this.formData,"tf_username",(this.$store.state.user && this.$store.state.user.username) || ""); this.$set(this.formData,"tf_caseCheckNeedSecondReply",0); this.$set(this.formData,"tf_secondReplyUnitIds",[]); this.loadReplyUnits(row.id) }
open(row, action) {
const defaultTaskNames = {
delegation: "团长审核",
preAudit: "校工会预审核",
feedbackEvaluation: "反馈评价",
unitReply: "承办单位答复",
caseCheck: "提案委员会审查"
}
const taskName = this.kind === "unitReply" ? (row.taskName || defaultTaskNames[this.kind]) : (row.curTaskName || row.taskName || defaultTaskNames[this.kind] || "")
this.$set(this, "bizId", row.id);
this.$set(this, "action", action);
this.$set(this, "formData", {
proposalId: row.id,
processTaskId: row.taskId || "",
taskKey: row.taskKey || "",
taskName: taskName,
instanceId: row.instanceId || "",
underTakeName: row.underTakeName || "",
underTakeIsMaster: !!row.underTakeIsMaster,
tf_opinion: "",
opinion: ""
})
if (action === "approval" && this.kind === "unitReply") {
// 承办单位答复附件上传入口暂时停用,保留初始化代码便于后续恢复。
// this.$set(this.formData, "tf_attachment", [])
this.loadCandidates(row.taskId || "")
}
if (action === "approval" && this.kind === "caseCheck") {
this.$set(this.formData, "tf_auditTime", moment().format("YYYY-MM-DD"));
this.$set(this.formData, "tf_username", (this.$store.state.user && this.$store.state.user.username) || "");
this.$set(this.formData, "tf_caseCheckNeedSecondReply", 0);
this.$set(this.formData, "tf_secondReplyUnitIds", []);
this.loadReplyUnits(row.id)
}
this.openLayer("detail")
},
loadCandidates(taskId) { this.$axios.post("/flow/common/candidate",{taskId:taskId}).then((res) => { if (res.code === 0) { this.$set(this,"candidates",res.data.candidates || []); this.$set(this,"supportCandidate",res.data.support) } }) },
confirmFeedback(value) { this.$set(this.formData,"tf_feedback_name",value.text); this.$set(this.formData,"tf_feedback",value.value); this.closeLayer("feedback-picker") },
confirmResult(value) { this.$set(this.formData,"result",value); this.closeLayer("result-picker") },
confirmImplement(value) { this.$set(this.formData,"tf_implementStateName",value.text); this.$set(this.formData,"tf_implementState",value.value); this.closeLayer("implement-picker") },
confirmCandidate(value) { this.$set(this.formData,"tf_nextNodeOperatorName",value.text); this.$set(this.formData,"tf_nextNodeOperator",value.value); this.closeLayer("candidate-picker") },
validateSecondReply(value) { return value === 0 || value === 1 },
validateSecondReplyUnits() { return this.formData.tf_caseCheckNeedSecondReply !== 1 || (this.formData.tf_secondReplyUnitIds || []).length > 0 },
onSecondReplyChange(value) { if (value !== 1) this.$set(this.formData,"tf_secondReplyUnitIds",[]) },
loadReplyUnits(proposalId) { this.$axios.post("/platform/proposal/case/check/listReplyUnits",{proposalId:proposalId}).then((res) => { if (res && res.code === 0) this.$set(this,"replyUnitOptions",res.data || []) }) },
openReplyUnitPicker() { this.$set(this,"pendingReplyUnitIds",[...(this.formData.tf_secondReplyUnitIds || [])]); this.openLayer("reply-unit-picker") },
toggleReplyUnit(item) { const ids=[...this.pendingReplyUnitIds]; const index=ids.indexOf(item.unitId); if (index>-1) ids.splice(index,1); else ids.push(item.unitId); this.$set(this,"pendingReplyUnitIds",ids) },
confirmReplyUnits() { this.$set(this.formData,"tf_secondReplyUnitIds",[...this.pendingReplyUnitIds]); this.closeLayer("reply-unit-picker") },
buildCaseCheckData() { const data={...this.formData}; const units=data.tf_caseCheckNeedSecondReply === 1 ? this.replyUnitOptions.filter((item) => data.tf_secondReplyUnitIds.indexOf(item.unitId)>-1) : []; const masters=units.filter((item) => item.isMaster); const slaves=units.filter((item) => !item.isMaster); data.tf_masterUnitIds=masters.map((item) => item.unitId); data.tf_masterUnitId=data.tf_masterUnitIds[0] || ""; data.tf_masterUnitNames=masters.map((item) => item.unitName); data.tf_masterUnitName=data.tf_masterUnitNames[0] || ""; data.tf_masterUnitNameStr=data.tf_masterUnitNames.join(","); data.tf_slaveUnitIds=slaves.map((item) => item.unitId); data.tf_slaveUnitNames=slaves.map((item) => item.unitName); data.tf_slaveUnitNameStr=data.tf_slaveUnitNames.join(","); return data },
loadCandidates(taskId) {
this.$axios.post("/flow/common/candidate", {taskId: taskId}).then((res) => {
if (res.code === 0) {
this.$set(this, "candidates", res.data.candidates || []);
this.$set(this, "supportCandidate", res.data.support)
}
})
},
confirmFeedback(value) {
this.$set(this.formData, "tf_feedback_name", value.text);
this.$set(this.formData, "tf_feedback", value.value);
this.closeLayer("feedback-picker")
},
confirmResult(value) {
this.$set(this.formData, "result", value);
this.closeLayer("result-picker")
},
confirmImplement(value) {
this.$set(this.formData, "tf_implementStateName", value.text);
this.$set(this.formData, "tf_implementState", value.value);
this.closeLayer("implement-picker")
},
confirmCandidate(value) {
this.$set(this.formData, "tf_nextNodeOperatorName", value.text);
this.$set(this.formData, "tf_nextNodeOperator", value.value);
this.closeLayer("candidate-picker")
},
validateSecondReply(value) {
return value === 0 || value === 1
},
validateSecondReplyUnits() {
return this.formData.tf_caseCheckNeedSecondReply !== 1 || (this.formData.tf_secondReplyUnitIds || []).length > 0
},
onSecondReplyChange(value) {
if (value !== 1) this.$set(this.formData, "tf_secondReplyUnitIds", [])
},
loadReplyUnits(proposalId) {
this.$axios.post("/platform/proposal/case/check/listReplyUnits", {proposalId: proposalId}).then((res) => {
if (res && res.code === 0) this.$set(this, "replyUnitOptions", res.data || [])
})
},
openReplyUnitPicker() {
this.$set(this, "pendingReplyUnitIds", [...(this.formData.tf_secondReplyUnitIds || [])]);
this.openLayer("reply-unit-picker")
},
toggleReplyUnit(item) {
const ids = [...this.pendingReplyUnitIds];
const index = ids.indexOf(item.unitId);
if (index > -1) ids.splice(index, 1); else ids.push(item.unitId);
this.$set(this, "pendingReplyUnitIds", ids)
},
confirmReplyUnits() {
this.$set(this.formData, "tf_secondReplyUnitIds", [...this.pendingReplyUnitIds]);
this.closeLayer("reply-unit-picker")
},
buildCaseCheckData() {
const data = {...this.formData};
const units = data.tf_caseCheckNeedSecondReply === 1 ? this.replyUnitOptions.filter((item) => data.tf_secondReplyUnitIds.indexOf(item.unitId) > -1) : [];
const masters = units.filter((item) => item.isMaster);
const slaves = units.filter((item) => !item.isMaster);
data.tf_masterUnitIds = masters.map((item) => item.unitId);
data.tf_masterUnitId = data.tf_masterUnitIds[0] || "";
data.tf_masterUnitNames = masters.map((item) => item.unitName);
data.tf_masterUnitName = data.tf_masterUnitNames[0] || "";
data.tf_masterUnitNameStr = data.tf_masterUnitNames.join(",");
data.tf_slaveUnitIds = slaves.map((item) => item.unitId);
data.tf_slaveUnitNames = slaves.map((item) => item.unitName);
data.tf_slaveUnitNameStr = data.tf_slaveUnitNames.join(",");
return data
},
prepareSubmit(value) {
// 各办理页面提交前统一校验意见字段;空白字符按未填写处理,提示后不再打开确认弹框。
const opinionConfig = this.kind === "suggestion"
@@ -120,24 +395,63 @@ const PROPOSAL_TASK_POPUP = {
this.$toast(opinionConfig.message)
return
}
this.$refs.formRef.validate().then(() => { this.$set(this,"pendingSubmitType",value); this.openLayer("submit-confirm") }).catch(() => {})
this.$refs.formRef.validate().then(() => {
this.$set(this, "pendingSubmitType", value);
this.openLayer("submit-confirm")
}).catch(() => {
})
},
/** 根据页面类型保留原接口和参数分支,成功后清理全部弹层历史并通知列表刷新。 */
submit() {
this.$set(this,"formLoading",true)
this.$set(this, "formLoading", true)
let request
if (this.kind === "suggestion") request=this.$axios.post("/platform/proposal/suggestion/suggest",this.formData)
else if (this.kind === "schoolLeaderApproval") { const data={...this.formData,tf_approval:this.pendingSubmitType}; delete data.taskKey; delete data.taskName; request=this.$axios.post("/platform/proposal/schoolLeaderApproval/executeTask",{data:JSON.stringify(data)}) }
else {
const data=this.kind === "caseCheck" ? {...this.buildCaseCheckData(),submitType:this.pendingSubmitType} : {...this.formData,submitType:this.pendingSubmitType}
if (this.kind === "preAudit") { data.taskName="startTask"; if (this.pendingSubmitType === 4) { data.tf_sourceTaskKey=this.formData.taskKey; data.tf_audit=false } }
if (this.kind === "unitReply") { delete data.underTakeIsMaster; delete data.underTakeName }
if (this.kind === "feedbackEvaluation" && data.tf_feedback === "DISSATISFIED") request=this.$axios.post("/platform/proposal/feedbackEvaluation/caseInfo",{instId:data.instanceId}).then((res) => { if (res.code !== 0) return Promise.reject(new Error(res.msg || "查询立案信息失败")); data.tf_masterUnitId=res.data.tf_masterUnitId; return this.$axios.post("/flow/common/executeTask",{data:JSON.stringify(data)}) })
else request=this.$axios.post("/flow/common/executeTask",{data:JSON.stringify(data)})
if (this.kind === "suggestion") request = this.$axios.post("/platform/proposal/suggestion/suggest", this.formData)
else if (this.kind === "schoolLeaderApproval") {
const data = {...this.formData, tf_approval: this.pendingSubmitType};
delete data.taskKey;
delete data.taskName;
request = this.$axios.post("/platform/proposal/schoolLeaderApproval/executeTask", {data: JSON.stringify(data)})
} else {
const data = this.kind === "caseCheck" ? {
...this.buildCaseCheckData(),
submitType: this.pendingSubmitType
} : {...this.formData, submitType: this.pendingSubmitType}
if (this.kind === "preAudit") {
data.taskName = "startTask";
if (this.pendingSubmitType === 4) {
data.tf_sourceTaskKey = this.formData.taskKey;
data.tf_audit = false
}
}
if (this.kind === "unitReply") {
delete data.underTakeIsMaster;
delete data.underTakeName
}
if (this.kind === "feedbackEvaluation" && data.tf_feedback === "DISSATISFIED") request = this.$axios.post("/platform/proposal/feedbackEvaluation/caseInfo", {instId: data.instanceId}).then((res) => {
if (res.code !== 0) return Promise.reject(new Error(res.msg || "查询立案信息失败"));
data.tf_masterUnitId = res.data.tf_masterUnitId;
return this.$axios.post("/flow/common/executeTask", {data: JSON.stringify(data)})
})
else request = this.$axios.post("/flow/common/executeTask", {data: JSON.stringify(data)})
}
request.then((res) => { if (res && res.code === 0) { this.$toast.success(res.msg); window.h5HistoryLayerManager.closeAll(() => this.$emit("success")) } }).catch((error) => { if (error && error.message && error.message !== "cancel") this.$toast.fail(error.message) }).finally(() => { this.$set(this,"formLoading",false) })
request.then((res) => {
if (res && res.code === 0) {
this.$toast.success(res.msg);
window.h5HistoryLayerManager.closeAll(() => this.$emit("success"))
}
}).catch((error) => {
if (error && error.message && error.message !== "cancel") this.$toast.fail(error.message)
}).finally(() => {
this.$set(this, "formLoading", false)
})
}
},
created() { window.h5HistoryLayerManager.ensureRegistered(this.pageKey); this.$set(this,"unsubscribeHistoryLayer",window.h5HistoryLayerManager.subscribe((stack) => this.syncLayers(stack))) },
beforeDestroy() { if (this.unsubscribeHistoryLayer) this.unsubscribeHistoryLayer(); if (window.h5HistoryLayerManager.pageKey === this.pageKey) window.h5HistoryLayerManager.unregister(this.pageKey) }
created() {
window.h5HistoryLayerManager.ensureRegistered(this.pageKey);
this.$set(this, "unsubscribeHistoryLayer", window.h5HistoryLayerManager.subscribe((stack) => this.syncLayers(stack)))
},
beforeDestroy() {
if (this.unsubscribeHistoryLayer) this.unsubscribeHistoryLayer();
if (window.h5HistoryLayerManager.pageKey === this.pageKey) window.h5HistoryLayerManager.unregister(this.pageKey)
}
}
@@ -47,6 +47,190 @@ layout("/layouts/platform_h5.html"){
color: #1989fa;
}
/* Popup 内固定导航栏不依赖 placeholder 占位,避免遮挡邀请附议筛选框。 */
.proposal-invite-popup {
display: flex;
padding-top: 46px;
overflow: hidden;
box-sizing: border-box;
flex-direction: column;
}
/* 邀请附议人弹框固定展示工号、姓名查询入口,清空关键词后恢复完整人员列表。 */
.proposal-invite-search {
padding: 12px 12px 8px;
background-color: #f3f7fd;
}
.proposal-invite-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.proposal-invite-search .van-field__control {
color: #263548;
font-size: 14px;
}
.proposal-invite-search .van-field__control::placeholder {
color: #a7b0bf;
}
/* 邀请状态使用双分段切换,当前状态通过白色底和蓝色短线突出展示。 */
.proposal-invite-tabs {
padding: 4px 12px 8px;
background-color: #f3f7fd;
}
.proposal-invite-tabs .van-tabs__wrap {
height: 48px;
}
.proposal-invite-tabs .van-tabs__nav {
padding: 4px;
box-sizing: border-box;
border-radius: 14px;
background-color: #edf4ff;
}
.proposal-invite-tabs .van-tab {
height: 40px;
color: #6e7d92;
font-size: 14px;
line-height: 40px;
}
.proposal-invite-tabs .van-tab--active {
border-radius: 10px;
color: #1989fa;
font-weight: 600;
background-color: #fff;
box-shadow: 0 4px 10px rgba(31, 89, 164, .08);
}
.proposal-invite-tabs .van-tabs__line {
bottom: 4px;
width: 30px;
height: 3px;
border-radius: 3px;
background-color: #1989fa;
}
/* 人员卡片区域独立滚动,顶部筛选区和底部操作区始终保留在视口内。 */
.proposal-invite-scroll {
min-height: 0;
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
/* 人员列表以独立卡片呈现,姓名、工号、单位和勾选区域层级清晰。 */
.proposal-invite-list {
padding: 0 12px 12px;
}
.proposal-invite-list .van-cell-group {
background-color: transparent;
}
.proposal-invite-person {
min-height: 70px;
margin-top: 12px;
padding: 12px 14px;
align-items: center;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 6px 16px rgba(43, 73, 112, .08);
}
.proposal-invite-person::after {
display: none;
}
.proposal-invite-person__title {
display: block;
color: #253247;
font-size: 15px;
font-weight: 600;
line-height: 22px;
}
.proposal-invite-person__login-name {
color: #79879a;
font-size: 14px;
font-weight: 400;
}
.proposal-invite-person__unit {
display: block;
margin-top: 3px;
color: #8c99aa;
font-size: 12px;
line-height: 18px;
}
.proposal-invite-person__checkbox {
display: flex;
width: 36px;
height: 36px;
align-items: center;
justify-content: center;
}
.proposal-invite-person__checkbox .van-icon {
width: 20px;
height: 20px;
box-sizing: border-box;
border: 1px solid #4b8cff;
border-radius: 50%;
background-color: #fff;
}
.proposal-invite-person__checkbox .van-checkbox__icon--checked .van-icon {
border-color: #1989fa;
background-color: #1989fa;
}
/* 分页和邀请按钮沿用同一圆角语言,与人员卡片形成统一的操作区域。 */
.proposal-invite-actions {
padding: 0 12px calc(12px + env(safe-area-inset-bottom));
background-color: #f3f7fd;
flex: none;
}
.proposal-invite-pagination {
margin-top: 2px;
}
.proposal-invite-pagination .van-pagination__item {
min-height: 36px;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 4px 12px rgba(43, 73, 112, .06);
}
.proposal-invite-pagination .van-pagination__item--active,
.proposal-invite-pagination .van-pagination__item:not(.van-pagination__item--disabled):active {
color: #fff;
background-color: #1989fa;
}
.proposal-invite-submit {
height: 42px;
margin-top: 12px;
border-radius: 12px;
}
.proposal-invite-empty {
min-height: 0;
overflow-y: auto;
flex: 1;
}
/* 详情弹框复用项目全屏 Popup 的固定导航结构,内容区域单独滚动。 */
.proposal-detail-popup {
overflow: hidden;
@@ -11,60 +11,51 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
left-arrow
left-text="返回"
fixed
placeholder
@click-left="$emit('close')">
</van-nav-bar>
<van-search
v-model="pageForm.searchKeyword"
class="proposal-invite-search"
:show-action="false"
shape="round"
clearable
placeholder="请输入工号或者姓名查询"
@search="doSearch">
@search="doSearch"
@clear="doSearch">
</van-search>
<van-tabs v-model="isInvite" @click="isInviteClick">
<van-tabs v-model="isInvite" class="proposal-invite-tabs" @click="isInviteClick">
<van-tab title="已邀请" name="1"></van-tab>
<van-tab title="可邀请" name="0"></van-tab>
</van-tabs>
<van-checkbox-group v-model="tableSelection" v-if="tableData && tableData.length > 0">
<van-cell-group>
<van-cell
v-for="(item, index) in tableData"
clickable
:key="item.loginName"
@click="tableRowToggle(index)">
<span slot="title">
{{item.userName}}{{item.loginName}}
</span>
<span slot="label" class="font-size0 van-color-text-assist">
{{item.unitName}}
</span>
<van-checkbox
:name="item"
:ref="'checkboxes' + item.loginName"
slot="right-icon"
v-if="!pageForm.isInvite">
</van-checkbox>
</van-cell>
</van-cell-group>
<div class="p10">
<van-pagination
class="proposal-soft-pagination"
v-model="pageForm.pageNumber"
:total-items="pageForm.totalCount"
:items-per-page="pageForm.pageSize"
@change="pageData">
</van-pagination>
<van-button
@click="invite"
type="primary"
block
:disabled="pageForm.isInvite"
class="mt10 proposal-soft-button">
邀请
</van-button>
</div>
</van-checkbox-group>
<div class="proposal-invite-scroll" v-if="tableData && tableData.length > 0">
<van-checkbox-group class="proposal-invite-list" v-model="tableSelection">
<van-cell-group>
<van-cell
class="proposal-invite-person"
v-for="(item, index) in tableData"
clickable
:key="item.loginName"
@click="tableRowToggle(index)">
<span slot="title" class="proposal-invite-person__title">
{{item.userName}} <span class="proposal-invite-person__login-name">{{item.loginName}}</span>
</span>
<span slot="label" class="proposal-invite-person__unit">
{{item.unitName}}
</span>
<van-checkbox
:name="item"
:ref="'checkboxes' + item.loginName"
class="proposal-invite-person__checkbox"
slot="right-icon"
@click.stop
v-if="!pageForm.isInvite">
</van-checkbox>
</van-cell>
</van-cell-group>
</van-checkbox-group>
</div>
<!-- 空状态提示随已邀请 / 可邀请标签切换便于用户判断当前无数据的具体范围 -->
<div v-else class="proposal-invite-empty">
@@ -75,6 +66,24 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
</div>
</div>
<div class="proposal-invite-actions" v-if="tableData && tableData.length > 0">
<van-pagination
class="proposal-soft-pagination proposal-invite-pagination"
v-model="pageForm.pageNumber"
:total-items="pageForm.totalCount"
:items-per-page="pageForm.pageSize"
@change="pageData">
</van-pagination>
<van-button
@click="invite"
type="primary"
block
:disabled="pageForm.isInvite"
class="proposal-soft-button proposal-invite-submit">
邀请
</van-button>
</div>
<van-dialog
:value="confirmVisible"
title="提示"
@@ -102,7 +111,7 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
tableData: [],
pageForm: {
pageNumber: 1,
pageSize: 5,
pageSize: 6,
totalCount: 0,
pageOrderName: "",
pageOrderBy: "",
@@ -207,8 +216,16 @@ const PROPOSAL_INVITE_SECONDER_COMPONENT = {
this.doSearch()
},
/**
* 点击人员卡片时切换对应的附议人勾选状态
* index 为当前页人员索引已邀请状态或找不到对应复选框时不处理无返回值
*/
tableRowToggle(index) {
if (this.pageForm.isInvite || index < 0) return
const item = this.tableData[index]
const checkboxRef = item ? this.$refs["checkboxes" + item.loginName] : null
if (!checkboxRef || !checkboxRef[0]) return
checkboxRef[0].toggle()
},
/**
@@ -411,8 +411,8 @@ layout("/layouts/platform_h5.html"){
</van-form>
</section>
<div class="proposal-seconded-form-actions">
<van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="submitSeconded(false)">不同意</van-button>
<van-button type="primary" block :loading="formLoading" class="proposal-soft-button" @click="submitSeconded(true)">同意</van-button>
<van-button type="danger" block :loading="formLoading" class="proposal-soft-button proposal-soft-button--danger" @click="submitSeconded(false)">不同意</van-button>
</div>
</div>
</proposal-info>
@@ -237,6 +237,109 @@ layout("/layouts/platform_h5.html"){
box-shadow: 0 4px 12px rgba(43, 73, 112, .08), inset 0 1px 1px rgba(255, 255, 255, .82);
}
/* 选择转交人弹框沿用提案列表的圆角搜索框、人员卡片和插图空状态。 */
.proposal-transfer-user-popup .proposal-full-popup__page {
background-color: #f3f7fd;
}
.proposal-transfer-user-search {
padding: 12px 12px 8px;
background-color: #f3f7fd;
flex: none;
}
.proposal-transfer-user-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.proposal-transfer-user-search .van-field__control {
color: #263548;
font-size: 14px;
}
.proposal-transfer-user-search .van-field__control::placeholder {
color: #a7b0bf;
}
.proposal-transfer-user-scroll {
min-height: 0;
overflow-y: auto;
flex: 1;
-webkit-overflow-scrolling: touch;
}
.proposal-transfer-user-list {
padding: 0 12px 12px;
background-color: transparent;
}
.proposal-transfer-user-item {
min-height: 64px;
margin-top: 12px;
padding: 12px 14px;
align-items: center;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 6px 16px rgba(43, 73, 112, .08);
}
.proposal-transfer-user-item::after {
display: none;
}
.proposal-transfer-user-item__name {
display: block;
color: #253247;
font-size: 15px;
font-weight: 600;
line-height: 22px;
}
.proposal-transfer-user-item__unit {
color: #79879a;
font-size: 13px;
font-weight: 400;
}
.proposal-transfer-user-empty {
display: flex;
min-height: 420px;
padding: 40px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.proposal-transfer-user-empty img {
display: block;
width: 82%;
max-width: 270px;
height: auto;
object-fit: contain;
}
.proposal-transfer-user-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
font-weight: 600;
line-height: 22px;
}
.proposal-transfer-user-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
@media (max-width: 350px) {
.proposal-unit-reply-content,
.proposal-unit-reply-sticky {
@@ -340,7 +443,7 @@ layout("/layouts/platform_h5.html"){
<div class="action-btn" v-if="!pageForm.approval && $auth.hasRoleOr('SYSADMIN,PROPOSAL_UNIT_LEADER')"
@click="openTransfer(row)">
<van-icon class="proposal-action-icon" name="exchange"></van-icon>
<span></span>
<span></span>
</div>
</template>
</table-list>
@@ -350,6 +453,16 @@ layout("/layouts/platform_h5.html"){
<transfer ref="transferRef" @success="doSearch"></transfer>
<proposal-task-popup ref="taskPopup" kind="unitReply" page-key="proposal-unit-reply-task" @success="doSearch"></proposal-task-popup>
<van-dialog
:value="communicationConfirmVisible"
title="提示"
message="是否已与提案代表进行充分沟通?"
confirm-button-text="是"
cancel-button-text="否"
show-cancel-button
@confirm="confirmCommunication"
@cancel="closeCommunicationConfirm">
</van-dialog>
</div>
<script nonce="${cspNonce!}">
<!--#include("transfer.js"){}#-->
@@ -378,6 +491,10 @@ layout("/layouts/platform_h5.html"){
{text: "未审核", value: "0"},
{text: "已审核", value: "1"}
],
communicationConfirmVisible: false,
pendingApprovalRow: null,
historyLayerPageKey: "proposal-unit-reply",
unsubscribeHistoryLayer: null
}
},
components: {
@@ -414,7 +531,29 @@ layout("/layouts/platform_h5.html"){
},
onApproval(row, index) {
this.$refs.taskPopup.open(row, "approval")
// row 为当前待答复提案;确认已充分沟通后,才进入原有答复表单。
this.$set(this, "pendingApprovalRow", row)
window.h5HistoryLayerManager.open("communication-confirm")
},
/** stack 为当前页面弹层名称数组;同步沟通确认框状态,无返回值。 */
syncHistoryLayers(stack) {
const visible = stack.includes("communication-confirm")
this.$set(this, "communicationConfirmVisible", visible)
if (!visible) this.$set(this, "pendingApprovalRow", null)
},
// 取消确认时同步回退浏览器历史,手机返回键与弹窗按钮行为保持一致。
closeCommunicationConfirm() {
window.h5HistoryLayerManager.close("communication-confirm")
},
// 确认后先关闭沟通弹层,再使用暂存的提案数据打开原有答复表单。
confirmCommunication() {
const row = this.pendingApprovalRow
window.h5HistoryLayerManager.close("communication-confirm", () => {
if (row) this.$refs.taskPopup.open(row, "approval")
})
},
onRevoke(row) {
@@ -445,6 +584,10 @@ layout("/layouts/platform_h5.html"){
}
},
created() {
window.h5HistoryLayerManager.ensureRegistered(this.historyLayerPageKey)
this.$set(this, "unsubscribeHistoryLayer", window.h5HistoryLayerManager.subscribe((stack) => {
this.syncHistoryLayers(stack)
}))
const tab = new URLSearchParams(window.location.search).get("tab")
if (tab === "done") {
this.$set(this.pageForm, "approval", true)
@@ -453,6 +596,12 @@ layout("/layouts/platform_h5.html"){
this.$set(this.pageForm, "approval", false)
this.$set(this.pageForm, "approvalText", "0")
}
},
beforeDestroy() {
if (this.unsubscribeHistoryLayer) this.unsubscribeHistoryLayer()
if (window.h5HistoryLayerManager.pageKey === this.historyLayerPageKey) {
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
}
})
</script>
@@ -1,28 +1,58 @@
const transfer = {
template: /*language=HTML*/ `
<van-popup v-model="showTransferDialog" position="right" class="proposal-full-popup" :close-on-click-overlay="false" get-container="#app">
<van-popup v-model="showTransferDialog" position="right" class="proposal-full-popup proposal-transfer-dialog" :close-on-click-overlay="false" get-container="#app">
<div class="proposal-full-popup__page">
<van-nav-bar @click-left="closeTransfer" left-arrow left-text="返回" title="提案转办" placeholder fixed></van-nav-bar>
<div class="proposal-full-popup__scroll transfer-dialog">
<div class="proposal-info-card">
<div class="info-item"><label>提案编号</label><span>{{transferForm.code}}</span></div>
<div class="info-item"><label>提案名称</label><span>{{transferForm.name}}</span></div>
<div class="info-item"><label>代表团</label><span>{{transferForm.delegationName}}</span></div>
</div>
<div class="proposal-info-card">
<van-field v-model="searchKeyword" label="转交给" placeholder="搜索用户" required readonly is-link @click="openUserList"></van-field>
</div>
<section class="proposal-view-card proposal-transfer-info-card">
<van-cell-group :border="false">
<van-cell title="提案编号">{{transferForm.code || '--'}}</van-cell>
<van-cell title="提案名称">{{transferForm.name || '--'}}</van-cell>
<van-cell title="代表团">{{transferForm.delegationName || '--'}}</van-cell>
</van-cell-group>
</section>
<section class="proposal-view-card proposal-transfer-user-card">
<van-cell-group :border="false">
<van-field v-model="searchKeyword" label="转交给" placeholder="搜索用户" required readonly is-link @click="openUserList"></van-field>
</van-cell-group>
</section>
</div>
<div class="button-group proposal-transfer-button-group">
<van-button block type="info" class="proposal-soft-button" @click="handleTransfer">提交</van-button>
<van-button block plain class="proposal-transfer-cancel-button" @click="closeTransfer">返回</van-button>
</div>
<div class="button-group proposal-transfer-button-group"><van-button block plain class="proposal-transfer-cancel-button" @click="closeTransfer">取消</van-button><van-button block type="info" class="proposal-soft-button" @click="handleTransfer"></van-button></div>
</div>
<van-popup v-model="showUserList" position="right" class="proposal-full-popup" :close-on-click-overlay="false" get-container="#app">
<van-popup v-model="showUserList" position="right" class="proposal-full-popup proposal-transfer-user-popup" :close-on-click-overlay="false" get-container="#app">
<div class="proposal-full-popup__page">
<van-nav-bar @click-left="closeUserList" left-arrow left-text="返回" title="选择转交人" placeholder fixed></van-nav-bar>
<div class="proposal-full-popup__scroll">
<van-search v-model="keyword" placeholder="搜索用户" @search="selectTransferUser(keyword)"></van-search>
<div class="proposal-view-main">
<div v-if="transferUserOptions.length" class="proposal-info-card"><van-cell v-for="item in transferUserOptions" :key="item.id" :title="item.username" @click="selectUser(item)"></van-cell></div>
<van-empty v-else class="empty-tip">请搜索需要转办的用户</van-empty>
<van-search
v-model="keyword"
class="proposal-transfer-user-search"
shape="round"
clearable
placeholder="请输入姓名或工号搜索"
@search="selectTransferUser(keyword)"
@clear="resetTransferUserSearch">
</van-search>
<div class="proposal-transfer-user-scroll">
<van-cell-group v-if="transferUserOptions.length" class="proposal-transfer-user-list" :border="false">
<van-cell
v-for="item in transferUserOptions"
:key="item.id"
class="proposal-transfer-user-item"
clickable
@click="selectUser(item)">
<span slot="title" class="proposal-transfer-user-item__name">
{{item.username}}<span class="proposal-transfer-user-item__unit">{{item.unitName || '--'}}</span>
</span>
</van-cell>
</van-cell-group>
<div v-else class="proposal-transfer-user-empty">
<img src="/assets/mobile/img/proposal/proposal-empty.png" alt="暂无转交用户插画">
<div class="proposal-transfer-user-empty__title">
{{hasUserSearched ? '暂无搜索记录' : '请搜索需要转办的用户'}}
</div>
<div v-if="hasUserSearched" class="proposal-transfer-user-empty__hint">请调整搜索条件后重试</div>
</div>
</div>
</div>
@@ -31,7 +61,7 @@ const transfer = {
</van-popup>
`,
data() {
return {showTransferDialog:false,transferForm:{},transferUserOptions:[],searchKeyword:"",showUserList:false,keyword:"",confirmVisible:false,historyLayerPageKey:"proposal-unit-reply-transfer",unsubscribeHistoryLayer:null}
return {showTransferDialog:false,transferForm:{},transferUserOptions:[],searchKeyword:"",showUserList:false,keyword:"",hasUserSearched:false,confirmVisible:false,historyLayerPageKey:"proposal-unit-reply-transfer",unsubscribeHistoryLayer:null}
},
computed: {
confirmMessage() {
@@ -43,12 +73,15 @@ const transfer = {
syncLayers(stack) { this.$set(this,"showTransferDialog",stack.includes("transfer")); this.$set(this,"showUserList",stack.includes("user-list")); this.$set(this,"confirmVisible",stack.includes("confirm")) },
openTransfer(row) { this.$set(this,"transferForm",{taskId:row.taskId,name:row.name,code:row.code,delegationName:row.delegationName}); this.$set(this,"searchKeyword",""); window.h5HistoryLayerManager.open("transfer") },
closeTransfer() { window.h5HistoryLayerManager.close("transfer") },
openUserList() { window.h5HistoryLayerManager.open("user-list") },
/** 打开用户列表前重置搜索条件和结果,确保首次进入展示搜索引导空状态。 */
openUserList() { this.resetTransferUserSearch(); window.h5HistoryLayerManager.open("user-list") },
closeUserList() { window.h5HistoryLayerManager.close("user-list") },
closeConfirm() { window.h5HistoryLayerManager.close("confirm") },
selectUser(item) { this.$set(this.transferForm,"userId",item.id); this.$set(this,"searchKeyword",item.username); this.closeUserList() },
/** keyword 为姓名关键字,接口返回可转交用户数组。 */
selectTransferUser(keyword) { this.$axios.post("/platform/proposal/unitReply/selectViceUser",{keyword:keyword}).then((res) => { if (res.code === 0) this.$set(this,"transferUserOptions",res.data || []) }) },
/** 清空姓名或工号关键词、搜索状态和人员结果,无返回值。 */
resetTransferUserSearch() { this.$set(this,"keyword",""); this.$set(this,"hasUserSearched",false); this.$set(this,"transferUserOptions",[]) },
/** keyword 为姓名或工号关键词;空值仅恢复搜索引导,非空值查询并返回可转交用户数组。 */
selectTransferUser(keyword) { const searchText=(keyword || "").trim(); if (!searchText) { this.resetTransferUserSearch(); return } this.$set(this,"hasUserSearched",true); this.$axios.post("/platform/proposal/unitReply/selectViceUser",{keyword:searchText}).then((res) => { if (res.code === 0) this.$set(this,"transferUserOptions",res.data || []) }) },
handleTransfer() { if (!this.transferForm.userId) { this.$toast("请选择转交用户"); return } window.h5HistoryLayerManager.open("confirm") },
/** 提交 taskId 和 userId 完成转办,成功后清理弹层并通知父列表刷新。 */
confirmTransfer() { this.$axios.post("/platform/proposal/unitReply/transfer",this.transferForm).then((res) => { if (res.code === 0) { this.$toast.success(res.msg); window.h5HistoryLayerManager.closeAll(() => this.$emit("success")) } }) }
@@ -146,12 +146,36 @@ layout("/layouts/platform_h5.html"){
this.userPoints = resp.data || 0
}
},
openSupplier(item) {
async openSupplier(item) {
if (!item.supplierUrl) {
this.$toast("暂无跳转地址")
return
}
window.location.href = item.supplierUrl
const userId = this.$store && this.$store.state.user ? this.$store.state.user.id : ""
if (!userId) {
this.$toast("获取用户信息失败,请重新登录后再试")
return
}
window.location.href = this.withSupplierQuery(item.supplierUrl, {
userId: userId
})
},
withSupplierQuery(url, params) {
const query = Object.keys(params)
.filter((key) => params[key] !== undefined && params[key] !== null && params[key] !== "")
.map((key) => encodeURIComponent(key) + "=" + encodeURIComponent(params[key]))
.join("&")
if (!query) {
return url
}
const hashIndex = url.indexOf("#")
if (hashIndex > -1) {
const base = url.slice(0, hashIndex)
const hash = url.slice(hashIndex)
const joiner = hash.indexOf("?") > -1 ? "&" : "?"
return base + hash + joiner + query
}
return url + (url.indexOf("?") > -1 ? "&" : "?") + query
},
onImgError(e) {
e.target.src = this.defaultLogo

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