diff --git a/pom.xml b/pom.xml index 67cce4dde..7e54be8cc 100644 --- a/pom.xml +++ b/pom.xml @@ -232,12 +232,14 @@ --> + diff --git a/src/main/java/com/rebuild/core/service/approval/ApprovalProcessor.java b/src/main/java/com/rebuild/core/service/approval/ApprovalProcessor.java index 3fe0ca2c3..2a86fa09f 100644 --- a/src/main/java/com/rebuild/core/service/approval/ApprovalProcessor.java +++ b/src/main/java/com/rebuild/core/service/approval/ApprovalProcessor.java @@ -190,9 +190,11 @@ public class ApprovalProcessor extends SetUser { ccs4share = nextNodes.getCcUsers4Share(this.getUser(), this.record, selectNextUsers); } + Set ccAccounts = nextNodes.getCcAccounts(this.record); + FlowNode currentNode = getFlowParser().getNode((String) stepApprover[2]); Application.getBean(ApprovalStepService.class) - .txApprove(approvedStep, currentNode.getSignMode(), ccs, nextApprovers, nextNode, addedData, checkUseGroup); + .txApprove(approvedStep, currentNode.getSignMode(), ccs, ccAccounts, nextApprovers, nextNode, addedData, checkUseGroup); // 非主事物 if (ccs4share != null) share2CcIfNeed(this.record, ccs4share); @@ -435,7 +437,7 @@ public class ApprovalProcessor extends SetUser { this.approval = status.getApprovalId(); Object[][] array = Application.createQueryNoFilter( - "select approver,state,remark,approvedTime,createdOn,createdBy,node,prevNode,nodeBatch,ccUsers from RobotApprovalStep" + + "select approver,state,remark,approvedTime,createdOn,createdBy,node,prevNode,nodeBatch,ccUsers,ccAccounts from RobotApprovalStep" + " where recordId = ? and isWaiting = 'F' and isCanceled = 'F' order by createdOn") .setParameter(1, this.record) .array(); @@ -537,6 +539,11 @@ public class ApprovalProcessor extends SetUser { for (ID u : (ID[]) step[9]) names.add(UserHelper.getName(u)); s.put("ccUsers", names); } + if (step.length > 10 && step[10] != null) { + List mobileOrEmails = new ArrayList<>(); + Collections.addAll(mobileOrEmails, step[10].toString().split(",")); + s.put("ccAccounts", mobileOrEmails); + } return s; } diff --git a/src/main/java/com/rebuild/core/service/approval/ApprovalStepService.java b/src/main/java/com/rebuild/core/service/approval/ApprovalStepService.java index 241627734..a0a7d9abe 100644 --- a/src/main/java/com/rebuild/core/service/approval/ApprovalStepService.java +++ b/src/main/java/com/rebuild/core/service/approval/ApprovalStepService.java @@ -10,6 +10,7 @@ package com.rebuild.core.service.approval; import cn.devezhao.bizz.privileges.impl.BizzPermission; import cn.devezhao.commons.CalendarUtils; import cn.devezhao.commons.ObjectUtils; +import cn.devezhao.commons.RegexUtils; import cn.devezhao.persist4j.Entity; import cn.devezhao.persist4j.PersistManagerFactory; import cn.devezhao.persist4j.Record; @@ -35,6 +36,7 @@ import com.rebuild.core.service.trigger.RobotTriggerManual; import com.rebuild.core.service.trigger.TriggerAction; import com.rebuild.core.service.trigger.TriggerWhen; import com.rebuild.core.support.i18n.Language; +import com.rebuild.core.support.integration.SMSender; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; @@ -123,12 +125,13 @@ public class ApprovalStepService extends InternalPersistService { * @param stepRecord * @param signMode * @param cc + * @param ccAccounts * @param nextApprovers [驳回时无需] * @param nextNode 下一节点或回退节点 * @param addedData [驳回时无需] * @param checkUseGroup [驳回时无需] */ - public void txApprove(Record stepRecord, String signMode, Set cc, Set nextApprovers, String nextNode, Record addedData, String checkUseGroup) { + public void txApprove(Record stepRecord, String signMode, Set cc, Set ccAccounts, Set nextApprovers, String nextNode, Record addedData, String checkUseGroup) { // 审批时更新主记录(驳回时不会传这个值) if (addedData != null) { GeneralEntityServiceContextHolder.setAllowForceUpdate(addedData.getPrimary()); @@ -158,6 +161,9 @@ public class ApprovalStepService extends InternalPersistService { if (cc != null && !cc.isEmpty()) { stepRecord.setIDArray("ccUsers", cc.toArray(new ID[0])); } + if (ccAccounts != null && !ccAccounts.isEmpty()) { + stepRecord.setString("ccAccounts", StringUtils.join(ccAccounts, ",")); + } super.update(stepRecord); final ID stepRecordId = stepRecord.getPrimary(); @@ -175,14 +181,30 @@ public class ApprovalStepService extends InternalPersistService { final String entityLabel = EasyMetaFactory.getLabel(MetadataHelper.getEntity(recordId.getEntityCode())); final String remark = stepRecord.getString("remark"); - // 抄送人 + // 抄送 + final String ccMsg = Language.L("用户 @%s 提交的 %s 审批已由 @%s %s,请知悉", + submitter, entityLabel, approver, Language.L(state)); + if (cc != null && !cc.isEmpty()) { - String ccMsg = Language.L("用户 @%s 提交的 %s 审批已由 @%s %s,请知悉", - submitter, entityLabel, approver, Language.L(state)); - if (StringUtils.isNotBlank(remark)) ccMsg += "\n > " + remark; + String innerMsg = ccMsg; + if (StringUtils.isNotBlank(remark)) innerMsg += "\n > " + remark; for (ID c : cc) { - Application.getNotifications().send(MessageBuilder.createApproval(c, ccMsg, recordId)); + Application.getNotifications().send(MessageBuilder.createApproval(c, innerMsg, recordId)); + } + } + // v3.2 外部人员 + if (ccAccounts != null && !ccAccounts.isEmpty()) { + String mobileMsg = MessageBuilder.formatMessage(ccMsg, Boolean.FALSE); + + String emailSubject = Language.L("审批通知"); + String emailMsg = ccMsg; + if (StringUtils.isNotBlank(remark)) emailMsg += "\n > " + remark; + emailMsg = MessageBuilder.formatMessage(emailMsg, Boolean.TRUE); + + for (String me : ccAccounts) { + if (SMSender.availableSMS() && RegexUtils.isCNMobile(me)) SMSender.sendSMSAsync(me, mobileMsg); + else if (SMSender.availableMail()) SMSender.sendMailAsync(me, emailSubject, emailMsg); } } diff --git a/src/main/java/com/rebuild/core/service/approval/FlowNode.java b/src/main/java/com/rebuild/core/service/approval/FlowNode.java index d28584bc8..edf9ec637 100644 --- a/src/main/java/com/rebuild/core/service/approval/FlowNode.java +++ b/src/main/java/com/rebuild/core/service/approval/FlowNode.java @@ -7,12 +7,15 @@ See LICENSE and COMMERCIAL in the project root for license information. package com.rebuild.core.service.approval; +import cn.devezhao.commons.RegexUtils; +import cn.devezhao.persist4j.Entity; import cn.devezhao.persist4j.Field; import cn.devezhao.persist4j.engine.ID; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.rebuild.core.Application; import com.rebuild.core.metadata.EntityHelper; +import com.rebuild.core.metadata.MetadataHelper; import com.rebuild.core.privileges.UserHelper; import com.rebuild.core.privileges.bizz.Department; import com.rebuild.utils.JSONUtils; @@ -146,9 +149,7 @@ public class FlowNode { */ public Set getSpecUsers(ID operator, ID record) { JSONArray userDefs = getDataMap().getJSONArray("users"); - if (userDefs == null || userDefs.isEmpty()) { - return Collections.emptySet(); - } + if (userDefs == null || userDefs.isEmpty()) return Collections.emptySet(); String userType = userDefs.getString(0); if (USER_SELF.equalsIgnoreCase(userType)) { @@ -216,6 +217,38 @@ public class FlowNode { return users; } + /** + * 获取外部抄送人(手机或邮箱) + * + * @param record + * @return + */ + public Set getCcAccounts(ID record) { + JSONArray accountFields = getDataMap().getJSONArray("accounts"); + if (accountFields == null || accountFields.isEmpty()) return Collections.emptySet(); + + Entity useEntity = MetadataHelper.getEntity(record.getEntityCode()); + List useFields = new ArrayList<>(); + + for (Object o : accountFields) { + if (MetadataHelper.getLastJoinField(useEntity, (String) o) != null) { + useFields.add((String) o); + } + } + if (useFields.isEmpty()) return Collections.emptySet(); + + Object[] o = Application.getQueryFactory().uniqueNoFilter(record, useFields.toArray(new String[0])); + if (o == null) return Collections.emptySet(); + + Set mobileOrEmail = new HashSet<>(); + for (Object me : o) { + if (RegexUtils.isCNMobile((String) me) || RegexUtils.isEMail((String) me)) { + mobileOrEmail.add((String) me); + } + } + return mobileOrEmail; + } + @Override public String toString() { String string = String.format("Id:%s, Type:%s", nodeId, type); diff --git a/src/main/java/com/rebuild/core/service/approval/FlowNodeGroup.java b/src/main/java/com/rebuild/core/service/approval/FlowNodeGroup.java index d8da37bc0..618ad7ec5 100644 --- a/src/main/java/com/rebuild/core/service/approval/FlowNodeGroup.java +++ b/src/main/java/com/rebuild/core/service/approval/FlowNodeGroup.java @@ -105,6 +105,21 @@ public class FlowNodeGroup { return users; } + /** + * @param recordId + * @return + */ + public Set getCcAccounts(ID recordId) { + Set mobileOrEmails = new HashSet<>(); + // 一般就一个,但不排除多个 CC 节点 + for (FlowNode node : nodes) { + if (FlowNode.TYPE_CC.equals(node.getType())) { + mobileOrEmails.addAll(node.getCcAccounts(recordId)); + } + } + return mobileOrEmails; + } + /** * @param operator * @param recordId diff --git a/src/main/java/com/rebuild/core/support/integration/SMSender.java b/src/main/java/com/rebuild/core/support/integration/SMSender.java index 10a109a0c..a3f495198 100644 --- a/src/main/java/com/rebuild/core/support/integration/SMSender.java +++ b/src/main/java/com/rebuild/core/support/integration/SMSender.java @@ -60,7 +60,7 @@ public class SMSender { try { sendMail(to, subject, content); } catch (Exception ex) { - log.error("Mail failed to send : {} < {}", to, subject, ex); + log.error("Email failed to send!", ex); } }); } @@ -131,7 +131,7 @@ public class SMSender { return emailId; } catch (EmailException ex) { - log.error("SMTP failed to send : {} > {}", to, subject, ex); + log.error("SMTP failed to send : {} | {} | {}", to, subject, content, ex); return null; } } @@ -157,7 +157,7 @@ public class SMSender { String r = OkHttpUtils.post("https://api-v4.mysubmail.com/mail/send.json", params); rJson = JSON.parseObject(r); } catch (Exception ex) { - log.error("Submail failed to send : {} > {}", to, subject, ex); + log.error("Submail failed to send : {} | {} | {}", to, subject, content, ex); return null; } @@ -166,12 +166,11 @@ public class SMSender { String sendId = ((JSONObject) returns.get(0)).getString("send_id"); createLog(to, logContent, TYPE_EMAIL, sendId, null); return sendId; - - } else { - log.error("Mail failed to send : {} > {}\nError : {}", to, subject, rJson); - createLog(to, logContent, TYPE_EMAIL, null, rJson.getString("msg")); - return null; } + + log.error("Submail failed to send : {} | {} | {}\nError : {}", to, subject, content, rJson); + createLog(to, logContent, TYPE_EMAIL, null, rJson.getString("msg")); + return null; } /** @@ -229,7 +228,7 @@ public class SMSender { try { sendSMS(to, content); } catch (Exception ex) { - log.error("SMS failed to send : {}", to, ex); + log.error("SMS failed to send!", ex); } }); } @@ -274,7 +273,7 @@ public class SMSender { String r = OkHttpUtils.post("https://api-v4.mysubmail.com/sms/send.json", params); rJson = JSON.parseObject(r); } catch (Exception ex) { - log.error("Subsms failed to send : {} > {}", to, content, ex); + log.error("Subsms failed to send : {} | {}", to, content, ex); return null; } finally { HeavyStopWatcher.clean(); @@ -284,12 +283,11 @@ public class SMSender { String sendId = rJson.getString("send_id"); createLog(to, content, TYPE_SMS, sendId, null); return sendId; - - } else { - log.error("SMS failed to send : {} > {}\nError : {}", to, content, rJson); - createLog(to, content, TYPE_SMS, null, rJson.getString("msg")); - return null; } + + log.error("Subsms failed to send : {} | {}\nError : {}", to, content, rJson); + createLog(to, content, TYPE_SMS, null, rJson.getString("msg")); + return null; } // @see com.rebuild.core.support.CommonsLog diff --git a/src/main/java/com/rebuild/web/robot/approval/ApprovalController.java b/src/main/java/com/rebuild/web/robot/approval/ApprovalController.java index 5d6e57a1f..d880e1f33 100644 --- a/src/main/java/com/rebuild/web/robot/approval/ApprovalController.java +++ b/src/main/java/com/rebuild/web/robot/approval/ApprovalController.java @@ -126,6 +126,7 @@ public class ApprovalController extends BaseController { JSONObject data = new JSONObject(); data.put("nextApprovers", formatUsers(approverList)); data.put("nextCcs", formatUsers(ccList)); + data.put("nextCcAccounts", nextNodes.getCcAccounts(recordId)); data.put("approverSelfSelecting", nextNodes.allowSelfSelectingApprover()); data.put("ccSelfSelecting", nextNodes.allowSelfSelectingCc()); data.put("isLastStep", nextNodes.isLastStep()); diff --git a/src/main/resources/metadata-conf.xml b/src/main/resources/metadata-conf.xml index 42ae61455..f5119416e 100644 --- a/src/main/resources/metadata-conf.xml +++ b/src/main/resources/metadata-conf.xml @@ -298,6 +298,7 @@ + diff --git a/src/main/resources/scripts/db-init.sql b/src/main/resources/scripts/db-init.sql index 6966c3294..296b6bb18 100644 --- a/src/main/resources/scripts/db-init.sql +++ b/src/main/resources/scripts/db-init.sql @@ -439,6 +439,7 @@ create table if not exists `robot_approval_step` ( `IS_BACKED` char(1) default 'F' comment '是否退回', `NODE_BATCH` varchar(100) comment '审批节点批次', `CC_USERS` varchar(420) comment '抄送人', + `CC_ACCOUNTS` varchar(500) comment '抄送外部人员', `MODIFIED_ON` timestamp not null default current_timestamp comment '修改时间', `MODIFIED_BY` char(20) not null comment '修改人', `CREATED_BY` char(20) not null comment '创建人', @@ -853,4 +854,4 @@ insert into `project_plan_config` (`CONFIG_ID`, `PROJECT_ID`, `PLAN_NAME`, `SEQ` -- DB Version (see `db-upgrade.sql`) insert into `system_config` (`CONFIG_ID`, `ITEM`, `VALUE`) - values ('021-9000000000000001', 'DBVer', 47); + values ('021-9000000000000001', 'DBVer', 49); diff --git a/src/main/resources/scripts/db-upgrade.sql b/src/main/resources/scripts/db-upgrade.sql index 20e5d0fca..5dbe8e600 100644 --- a/src/main/resources/scripts/db-upgrade.sql +++ b/src/main/resources/scripts/db-upgrade.sql @@ -1,6 +1,10 @@ -- Database upgrade scripts for rebuild 1.x and 2.x -- Each upgraded starts with `-- #VERSION` +-- #49 (v3.2) +alter table `robot_approval_step` + add column `CC_ACCOUNTS` varchar(500) comment '抄送外部人员'; + -- #47 (v3.1) alter table `robot_approval_step` add column `CC_USERS` varchar(420) comment '抄送人'; diff --git a/src/main/resources/web/assets/css/rb-page.css b/src/main/resources/web/assets/css/rb-page.css index 1443feca0..fbdc577cd 100644 --- a/src/main/resources/web/assets/css/rb-page.css +++ b/src/main/resources/web/assets/css/rb-page.css @@ -3030,6 +3030,21 @@ form { margin-top: 3px; } +.form.approval-form .cc-accounts { + color: #666; +} + +.form.approval-form .cc-accounts a { + color: #404040; +} + +.form.approval-form .cc-accounts a + a::before { + content: ','; + margin-left: 1px; + margin-right: 4px; + color: #8a8a8a; +} + .select2-sm .select2-container--default .select2-selection--single, .select2-sm .select2-container--default .select2-selection--multiple { min-height: 36px; @@ -4407,7 +4422,7 @@ html.external-auth .auth-body.must-center .login { .user-popup .infos p.phone::after, .user-popup .infos p.email::after { - font-family: 'Material-Design-Iconic-Font', serif; + font-family: 'Material Design Icons', serif; } .user-popup .infos p.phone::after, @@ -4418,12 +4433,11 @@ html.external-auth .auth-body.must-center .login { } .user-popup .infos p.phone::after { - content: '\f2d4'; + content: '\F011C'; } .user-popup .infos p.email::after { - content: '\f15a'; - transform: translateY(1px); + content: '\F01F0'; } .copied-check > .zmdi-copy::before { diff --git a/src/main/resources/web/assets/js/admin/approval-design.js b/src/main/resources/web/assets/js/admin/approval-design.js index 2a8573782..4b9d1d5c9 100644 --- a/src/main/resources/web/assets/js/admin/approval-design.js +++ b/src/main/resources/web/assets/js/admin/approval-design.js @@ -203,6 +203,7 @@ class SimpleNode extends NodeSpec { if (data.selfSelecting && data.users.length > 0) descs.push($L('允许自选')) if (data.ccAutoShare) descs.push($L('自动共享')) + if ((data.accounts || []).length > 0) descs.push(`${$L('外部人员')}(${data.accounts.length})`) if (this.nodeType === 'approver') descs.push(data.signMode === 'AND' ? $L('会签') : data.signMode === 'ALL' ? $L('依次审批') : $L('或签')) return ( @@ -764,10 +765,6 @@ class ApproverNodeConfig extends StartNodeConfig { // 抄送人 class CCNodeConfig extends StartNodeConfig { - constructor(props) { - super(props) - } - render() { return (
@@ -790,18 +787,45 @@ class CCNodeConfig extends StartNodeConfig { {$L('抄送人无读取权限时自动共享')}
+ +
+ + (this._UserSelector2 = c)} userType={2} hideUser hideDepartment hideRole hideTeam /> +

{$L('选择外部人员的电话(手机)或邮箱字段')}

+
+ {this.renderButton()} ) } + componentDidMount() { + super.componentDidMount() + + if ((this.props.accounts || []).length > 0) { + $.post(`/commons/search/user-selector?entity=${this.props.entity || wpc.applyEntity}`, JSON.stringify(this.props.accounts), (res) => { + if (res.error_code === 0 && res.data.length > 0) { + this._UserSelector2.setState({ selected: res.data }) + } + }) + } + } + save = () => { const d = { nodeName: this.state.nodeName, users: this._UserSelector.getSelected(), selfSelecting: this.state.selfSelecting, ccAutoShare: this.state.ccAutoShare, + accounts: this._UserSelector2.getSelected(), + } + + if (d.accounts.length > 1 && rb.commercial < 1) { + RbHighbar.error(WrapHtml($L('免费版不支持抄送给外部人员功能 [(查看详情)](https://getrebuild.com/docs/rbv-features)'))) + return } if (d.users.length === 0 && !d.selfSelecting) { @@ -1040,12 +1064,31 @@ class UserSelectorWithField extends UserSelector { super.componentDidMount() this._fields = [] - $.get(`/admin/robot/approval/user-fields?entity=${this.props.entity || wpc.applyEntity}`, (res) => { - this._fields = res.data || [] - }) + + // 外部人员 + if (this.props.userType === 2) { + $.get(`/commons/metadata/fields?deep=2&entity=${this.props.entity || wpc.applyEntity}`, (res) => { + res.data && + res.data.forEach((item) => { + if (item.type === 'PHONE' || item.type === 'EMAIL') { + this._fields.push({ id: item.name, text: item.label }) + } + }) + this.switchTab() + }) + } else { + $.get(`/admin/robot/approval/user-fields?entity=${this.props.entity || wpc.applyEntity}`, (res) => { + this._fields = res.data || [] + }) + } } switchTab(type) { + if (this.props.userType === 2) { + this.setState({ tabType: 'FIELDS', items: this._fields || [] }) + return + } + type = type || this.state.tabType if (type === 'FIELDS') { const q = this.state.query diff --git a/src/main/resources/web/assets/js/rb-approval.js b/src/main/resources/web/assets/js/rb-approval.js index 4e30d1795..9e4f8db8b 100644 --- a/src/main/resources/web/assets/js/rb-approval.js +++ b/src/main/resources/web/assets/js/rb-approval.js @@ -323,6 +323,17 @@ class ApprovalUsersForm extends RbFormHandler { (this._ccSelector = c)} /> )} + + {(this.state.nextCcAccounts || []).length > 0 && ( +
+ {$L('及以下外部人员')} +
+ {this.state.nextCcAccounts.map((me) => { + return {me} + })} +
+
+ )} )} @@ -734,16 +745,23 @@ class ApprovalStepViewer extends React.Component {

{item.remark}

)} - {item.ccUsers && item.state >= 10 && ( + {item.state >= 10 && (item.ccUsers || []).length + (item.ccAccounts || []).length > 0 && (

{$L('已抄送')} - {item.ccUsers.map((item) => { + {(item.ccUsers || []).map((item) => { return {item} })} + {(item.ccAccounts || []).map((item) => { + return ( + + {item} + + ) + })}

)} diff --git a/src/main/resources/web/assets/js/trigger/trigger.SENDNOTIFICATION.js b/src/main/resources/web/assets/js/trigger/trigger.SENDNOTIFICATION.js index b98e2e7e6..1a29c7035 100644 --- a/src/main/resources/web/assets/js/trigger/trigger.SENDNOTIFICATION.js +++ b/src/main/resources/web/assets/js/trigger/trigger.SENDNOTIFICATION.js @@ -60,6 +60,7 @@ class ContentSendNotification extends ActionContentSpec {
(this._sendTo2 = c)} hideUser hideDepartment hideRole hideTeam /> +

{$L('选择外部人员的电话(手机)或邮箱字段')}

@@ -180,12 +181,12 @@ class AccountSelectorWithField extends UserSelector { this._fields = [] $.get(`/commons/metadata/fields?deep=2&entity=${this.props.entity || wpc.sourceEntity}`, (res) => { - $(res.data).each((idx, item) => { - if (item.type === 'PHONE' || item.type === 'EMAIL') { - this._fields.push({ id: item.name, text: item.label }) - } - }) - + res.data && + res.data.forEach((item) => { + if (item.type === 'PHONE' || item.type === 'EMAIL') { + this._fields.push({ id: item.name, text: item.label }) + } + }) this.switchTab() }) }