0%

YTS测试

参考文档链接如下

https://uap-wiki.yyrd.com/pages/viewpage.action?pageId=177940639#yts%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E6%B5%8B%E8%AF%95%E7%94%A8%E4%BE%8B%E8%AF%B4%E6%98%8E%E6%96%87%E6%A1%A3-3.2.2Httpclient-tcc%E8%B0%83%E7%94%A8:

image-20230213142706813

调用方式:http://bip-daily.yyuap.com/http-send-demo/http/httpClientSave?uid=7&amount=20&orderId=02&ytsMode=tcc

3.2.2.1 无桩情况

通过查询所有节点status状态是否为confirmed;

查询语句:

select * from yts-http-client.yts_trans_xxxx where gtx_id=’#####’ ;

select * from yts-http-client.yts_rel_xxxx where gtx_id=’#####’ ;

数据库 期望状态
yts-http-client trans confirmed
rel confirmed
yts-http-provider trans confirmed
rel confirmed
yts-http-provider trans confirmed
rel confirmed

1.点击调用链接http://bip-daily.yyuap.com/http-send-demo/http/httpClientSave?uid=7&amount=20&orderId=02&ytsMode=tcc

出现如下结果:

image-20230213142821202

1
{"orderId":"02","gtxId":"20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b","message":"付款成功,请到对应库中验证"}

2.查询结果

1
select * from `yts-http-client`.yts_trans_20230213 where gtx_id='20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b';

pk, id, module_name, service_name, method_name, gtx_id, tx_id, ptx_id, cancel_method, type, status, create_time, interface_name, update_time, confirm_method, reported, context, env, call_tag, provider_id, invocation, all_confirmed, mode, retry_status, bill_no, trace_id, rule_id

‘139’, ‘ef8887d3-5bb3-45fb-af9d-18e39569120b’, ‘http-send-demo’, ‘http-send-demo’, ‘GET’, ‘20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b’, ‘59c4d67a-39a4-4e3b-ad56-75503bdd26f5’, NULL, NULL, ‘ROOT’, ‘CONFIRMED’, ‘1676268435439’, ‘com.yonyou.ucf.mdd.api.interfaces.rpc.IRuleApi’, ‘1676268451304’, NULL, ‘’, ‘{"rootProviderId":"c87e2267-1001-4c70-bb2a-ab41f3b81aa3","rootServiceName":"http-send-demo"}’, ‘combine’, NULL, ‘c87e2267-1001-4c70-bb2a-ab41f3b81aa3’, ?, ‘0’, ‘httpClient_tcc’, NULL, NULL, ‘1602eb0fceb13732’, ‘/yts-http-demo/yts/httpClientTccSave’

1
select * from `yts-http-client`.yts_rel_20230213 where gtx_id='20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b';

pk, id, module_name, call_service_name, call_interface_name, call_method_name, service_name, interface_name, method_name, gtx_id, ptx_id, tx_id, cancel_method, invocation, cancel_invocation, provider_id, server_provider_id, create_time, update_time, confirm_method, env, parent_pk, call_tag, mode, retry_count, status, retry_status, reported, bill_no, trace_id, rule_id, confirm

139‘, ‘2f00baa6-6b22-4489-bac2-da9095c820dc’, ‘http://bip-daily.yyuap.com', ‘http-send-demo’, NULL, NULL, ‘http://bip-daily.yyuap.com', ‘/yts-http-demo/yts/httpClientTccSave’, ‘GET‘, ‘20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b‘, ‘59c4d67a-39a4-4e3b-ad56-75503bdd26f5‘, ‘6f8e15fc-f9b2-4545-b022-2c2f422340e9‘, ‘cancel’, ?, ?, ‘c87e2267-1001-4c70-bb2a-ab41f3b81aa3’, NULL, ‘1676268435504’, ‘1676268451238’, NULL, ‘combine’, ‘139’, NULL, ‘httpClient_tcc’, ‘1’, ‘CONFIRMED‘, NULL, NULL, NULL, NULL, NULL, ‘0’

1
select * from `yts-http-provider`.yts_trans_20230213 where gtx_id='20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b';

pk, id, module_name, service_name, method_name, gtx_id, tx_id, ptx_id, cancel_method, type, status, create_time, interface_name, update_time, confirm_method, reported, context, env, call_tag, provider_id, invocation, all_confirmed, mode, retry_status, bill_no, trace_id, rule_id

214‘, ‘e7dac527-896c-4be6-9a47-dc6034b927fd’, ‘yts-http-provider’, ‘yts-http-provider’, ‘GET’, ‘20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b‘, ‘6f8e15fc-f9b2-4545-b022-2c2f422340e9‘, ‘59c4d67a-39a4-4e3b-ad56-75503bdd26f5‘, NULL, ‘BRANCH’, ‘CONFIRMED‘, ‘1676268450611’, ‘com.yonyou.ucf.mdd.api.interfaces.rpc.IRuleApi’, ‘1676268451184’, NULL, ‘’, ‘{"rootProviderId":"c87e2267-1001-4c70-bb2a-ab41f3b81aa3","rootServiceName":"http-send-demo"}’, ‘combine’, ‘’, ‘c87e2267-1001-4c70-bb2a-ab41f3b81aa3’, ?, ‘0’, ‘httpClient_tcc’, ‘’, NULL, ‘237b53c582d36d47’, ‘/yts/httpClientTccSave’

1
select * from `yts-http-provider`.yts_rel_20230213 where gtx_id='20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b';

pk, id, module_name, call_service_name, call_interface_name, call_method_name, service_name, interface_name, method_name, gtx_id, ptx_id, tx_id, cancel_method, invocation, cancel_invocation, provider_id, server_provider_id, create_time, update_time, confirm_method, env, parent_pk, call_tag, mode, retry_count, status, retry_status, reported, bill_no, trace_id, rule_id, confirm

101‘, ‘6d7efcc1-9e9b-4a2a-95cf-d63987502461’, ‘http://bip-daily.yyuap.com', ‘yts-http-provider’, NULL, NULL, ‘http://bip-daily.yyuap.com', ‘/yts-http-privider2/yts/httpClientTccSave’, ‘GET’, ‘20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b’, ‘6f8e15fc-f9b2-4545-b022-2c2f422340e9’, ‘649a5ace-30f3-4ab6-bf77-6efced96ff1f‘, ‘cancel’, ?, ?, ‘c87e2267-1001-4c70-bb2a-ab41f3b81aa3’, NULL, ‘1676268450678’, ‘1676268451096’, NULL, ‘combine’, ‘214’, NULL, ‘httpClient_tcc’, ‘1’, ‘CONFIRMED’, NULL, NULL, NULL, NULL, NULL, ‘0’

1
select * from `yts-http-provider2`.yts_trans_20230213 where gtx_id='20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b';

pk, id, module_name, service_name, method_name, gtx_id, tx_id, ptx_id, cancel_method, type, status, create_time, interface_name, update_time, confirm_method, reported, context, env, call_tag, provider_id, invocation, all_confirmed, mode, retry_status, bill_no, trace_id, rule_id

158‘, ‘afa9ae81-cc3f-45d9-8bae-a6b8f48c32d4’, ‘yts-http-privider2’, ‘yts-http-privider2’, ‘GET’, ‘20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b‘, ‘649a5ace-30f3-4ab6-bf77-6efced96ff1f‘, ‘6f8e15fc-f9b2-4545-b022-2c2f422340e9‘, NULL, ‘BRANCH’, ‘CONFIRMED‘, ‘1676268450745’, ‘com.yonyou.ucf.mdd.api.interfaces.rpc.IRuleApi’, ‘1676268451070’, NULL, ‘’, ‘{"rootProviderId":"c87e2267-1001-4c70-bb2a-ab41f3b81aa3","rootServiceName":"http-send-demo"}’, ‘combine’, ‘’, ‘c87e2267-1001-4c70-bb2a-ab41f3b81aa3’, ?, ‘0’, ‘httpClient_tcc’, ‘’, NULL, ‘a7f0390cf8ce46e0’, ‘/yts/httpClientTccSave’

1
select * from `yts-http-provider2`.yts_rel_20230213 where gtx_id='20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b';

没查到,表是空表

trans

yts-http-client yts-http-provider ‘yts-http-privider2’
gtx_id 20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b 20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b 20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b
tx_id 59c4d67a-39a4-4e3b-ad56-75503bdd26f5 6f8e15fc-f9b2-4545-b022-2c2f422340e9 649a5ace-30f3-4ab6-bf77-6efced96ff1f
ptx_id NULL 59c4d67a-39a4-4e3b-ad56-75503bdd26f5 6f8e15fc-f9b2-4545-b022-2c2f422340e9

rel

yts-http-client yts-http-provider ‘yts-http-privider2’
gtx_id 20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b 20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b 20230213140715@98ff5cb0-7060-46c1-87c5-523ca58fb99b
tx_id 59c4d67a-39a4-4e3b-ad56-75503bdd26f5 6f8e15fc-f9b2-4545-b022-2c2f422340e9
ptx_id 6f8e15fc-f9b2-4545-b022-2c2f422340e9 649a5ace-30f3-4ab6-bf77-6efced96ff1f

git拉取yms-library项目

image-20230209103638993

问题

运行报错找不到sql.translator.impl

询问诗涵解决

image-20230209103722555

已解决

进行test排查异常报错等

测试过程

测试test.BaseDAOTest#testUpdate

image-20230209110353196

存疑 有问题(已解决)

测试test.BaseDAOTest#testUpdate3

同上

测试test.BaseDAOTest#testUpdate2

同上

测试test.BaseDAOTest#testUpdate4

测试成功,实现update持久化

image-20230209135614895

1
2
3
4
5
6
7
8
@Test
public void testUpdate4() throws DAOException {
SQLParameter parameter = new SQLParameter();
parameter.addParam("btax_test5");
parameter.addParam("0000026a-f46f-4eee-98a7-ff4d4114bcf3");
parameter.addParam("2022-07-21 18:45:09.0");
baseDAO.update("update ipuquotation_test set btax=? where ipuquotaionid=? and pubts=? ", parameter);
}
  • 梳理流程

测试test.BaseDAOTest#testSaveWithPk

测试成功

image-20230209135550209

1
2
3
4
5
6
7
8
@Test
public void testSaveWithPk() throws DAOException {
//IpuQuotation quotation = baseDAO.queryByPK(IpuQuotation.class, "2", new String[]{"ipuquotaionid", "btax"});
IpuQuotation quotation1 = new IpuQuotation();
quotation1.setBtax("btax_test");
quotation1.setIpuquotaionid("5");
baseDAO.insertWithPK(quotation1);
}
  • 梳理流程

测试test.BaseDAOTest#testSave

测试成功

image-20230209135745373

1
2
3
4
5
6
7
@Test
public void testSave() throws DAOException {
IpuQuotation quotation = new IpuQuotation();
quotation.setBtax("btax_test1");
quotation.setIpuquotaionid("test_save12");
baseDAO.insert(quotation);
}
  • 梳理流程

测试test.BaseDAOTest#testSaveList

测试test.BaseDAOTest#testSaveList2

测试test.BaseDAOTest#testDelete

测试成功

image-20230209143756540

image-20230209143805344

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testDelete() throws DAOException {
final IpuQuotation ipuQuotation = new IpuQuotation();
ipuQuotation.setIpuquotaionid("test2");
final IpuQuotation ipuQuotation2 = new IpuQuotation();
ipuQuotation2.setIpuquotaionid("test1");
baseDAO.remove(new ArrayList<IpuQuotation>() {
{
add(ipuQuotation);
add(ipuQuotation2);
}
});
}

测试test.BaseDAOTest#testQueryAll

测试成功

image-20230209144618751

1
2
3
4
5
6
@Test
public void testQueryAll() throws DAOException {
IpuQuotation ipuQuotation = new IpuQuotation();
List<IpuQuotation> quotation = baseDAO.queryAll(ipuQuotation);
System.out.println(quotation.size());
}

测试test.BaseDAOTest#testQueryByClause

测试成功

image-20230209144735753

1
2
3
4
5
6
7
8
9
10
@Test
public void testQueryByClause2() throws DAOException {
SQLParameter parameter = new SQLParameter();
parameter.addParam("btax_test");
IpuQuotation ipuQuotation = new IpuQuotation();
List<IpuQuotation> ipuQuotations =
baseDAO.queryByClause(ipuQuotation, "select * from ipuquotation_test" + " where btax=?",
parameter);
System.out.println(ipuQuotations.size());
}

测试test.BaseDAOTest#testQueryByClause2

测试成功

image-20230209144933387

image-20230209144857363

1
2
3
4
5
6
7
8
9
10
@Test
public void testQueryByClause2() throws DAOException {
SQLParameter parameter = new SQLParameter();
parameter.addParam("btax_test");
IpuQuotation ipuQuotation = new IpuQuotation();
List<IpuQuotation> ipuQuotations =
baseDAO.queryByClause(ipuQuotation, "select * from ipuquotation_test" + " where btax=?",
parameter);
System.out.println(ipuQuotations.size());
}

问题梳理总结

queryByPK

1
2
3
4
5
6
7
@Test
public void testUpdate() throws DAOException {
IpuQuotation ipuQuotation = new IpuQuotation();
IpuQuotation quotation = baseDAO.queryByPK(ipuQuotation, "2");
quotation.setBtax("btax_test4");
baseDAO.update(quotation, new String[]{"btax"});
}

com.yonyou.iuap.yms.dao.AbstractDAO#queryByPK(com.yonyou.iuap.yms.param.BaseEntity, ID)

1
2
3
4
5
6
7
8
9
10
/**
* 根据PK查询指定VO
*
* @param baseEntity
* @param pk 主键
*/
@Override
public <T> T queryByPK(BaseEntity baseEntity, ID pk) throws DAOException {
return queryByPK(baseEntity, pk, null);
}

com.yonyou.iuap.yms.dao.AbstractDAO#queryByPK(com.yonyou.iuap.yms.param.BaseEntity, ID, java.lang.String[])

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据主键返回指定列的VO对象
*/
@Override
public <T> T queryByPK(BaseEntity baseEntity, ID pk, String[] selectedFields) throws DAOException {
try {
manager = createPersistenceManager();
return (T) manager.retrieveByPK(baseEntity, pk, selectedFields);
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw new DAOException(e.getMessage());
}
}

com.yonyou.iuap.yms.jdbc.AbstractJdbcPersistenceManager#retrieveByPK(com.yonyou.iuap.yms.param.BaseEntity, ID, java.lang.String[])

Entity entity = MetaEntityHelper.getMetaEntity(fullName);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/**
* 根据主键查询
*/
public <T, ID> T retrieveByPK(BaseEntity baseEntity, ID pk, String[] selectedFields) throws Exception {
if (pk == null) {
throw new IllegalArgumentException("pk is null");
}
T result = null;
boolean ormAttributeSign = BeanDataOperateHelper.initOrmAttributeSign(baseEntity);
Map<String, Object> resultMap = null;
SQLParameter param = new SQLParameter();
param.addParam(pk);
Class clz = baseEntity.getClass();
String fullName = baseEntity.getFullName();
Map<String, ORMColumnInfo> types = findColumnInfo(clz, fullName);
Entity entity = MetaEntityHelper.getMetaEntity(fullName);
List<Property> elasticAttributes = entity.getElasticAttributes();
//buildSQL
for (Property elasticProperty : elasticAttributes) {
types.remove(elasticProperty.columnName());
}
StringBuffer columnsForQuery = new StringBuffer();
for (String columnName : types.keySet()) {
columnsForQuery.append(columnName).append(",");
}
columnsForQuery.delete(columnsForQuery.length() - 1, columnsForQuery.length());

List<OneToOneType> parallelTypes = BizMetaHelper.getOneToOneField(baseEntity, null, null);
List<OneToOneType> characteristicTypes = BeanProcessorHelper.getCharacteristicOneToOneField(fullName);
String condition = BeanProcessorHelper.getPkColumn(baseEntity.getClass(), baseEntity.getFullName()) + "=?";

if (CollectionUtils.isEmpty(parallelTypes) && CollectionUtils.isEmpty(characteristicTypes)) {
String pkName = BeanProcessorHelper.getPkColumn(clz, fullName);
boolean hasPKField = false;
StringBuffer mainSQL = new StringBuffer();
String tableName = BeanProcessorHelper.getTableName(clz, fullName);
if (selectedFields == null) {
mainSQL.append("SELECT ").append(columnsForQuery).append(" FROM ").append(tableName);
} else {

selectedFields = transPropertyNameToFieldNames(clz, fullName, selectedFields);
String[] columns = getValidNames(fullName, clz, types, baseEntity, selectedFields);
mainSQL.append("SELECT ");
for (int i = 0; i < columns.length; i++) {
if (columns[i] != null) {
mainSQL.append(columns[i]).append(",");
if (columns[i].equalsIgnoreCase(pkName)) {
hasPKField = true;
}
}
}
if (!hasPKField) {
mainSQL.append(pkName).append(",");
}
mainSQL.setLength(mainSQL.length() - 1);
mainSQL.append(" FROM ").append(tableName);
}
mainSQL.append(" WHERE ").append(condition);
BaseProcessor processor = new BeanListProcessor(baseEntity);
List<T> results = getJdbcSession().executeQuery(mainSQL.toString(), param, processor);
if (results.size() >= 1) {
result = results.get(0);
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("query for oneToOne .. {} ", clz.getClasses());
}
Field[] fields = baseEntity.getClass().getDeclaredFields();
String[] propNames = new String[fields.length];
for (int i = 0; i < fields.length; i++) {
propNames[i] = fields[i].getName();
}
String pkName = BeanProcessorHelper.getPkColumn(clz, fullName);
boolean hasPKField = false;
StringBuffer mainSQL = new StringBuffer();
String tableName = BeanProcessorHelper.getTableName(clz, fullName);
StringBuilder mapSQL = new StringBuilder();
mapSQL.append("SELECT * FROM ").append(tableName).append(" WHERE ").append(condition);
List<Map> mapList = getJdbcSession().executeQuery(mapSQL.toString(), param, new MapListProcessor());
if (mapList.size() >= 1) {
resultMap = mapList.get(0);
} else {
logger.debug("查询结果为空!");
return null;
}
if (selectedFields == null) {
mainSQL.append("SELECT ").append(columnsForQuery).append(" FROM ").append(tableName).append(" WHERE ").append(condition);
List<T> results = getJdbcSession().executeQuery(mainSQL.toString(), param, new BeanListProcessor(baseEntity));
if (results.size() >= 1) {
result = results.get(0);
}
//特征
for (OneToOneType oneToOneType : characteristicTypes) {
String childSQL = buildSQLWithoutSelectFields(oneToOneType, clz, fullName, resultMap);
if (StringUtils.isNotEmpty(childSQL)) {
characteristicEntityHandler(baseEntity, childSQL, fullName, result, ormAttributeSign);
}
}
//平行
for (OneToOneType oneToOneType : parallelTypes) {
String childTable = BeanProcessorHelper.getTableName(oneToOneType.getTargetEntity(), oneToOneType.getTargetFullName());
String childSQL = buildSQLWithoutSelectFields(oneToOneType, clz, fullName, resultMap);
if (StringUtils.isNotEmpty(childSQL)) {
parallelEntityHandler(baseEntity, childSQL, childTable, result, ormAttributeSign);
}
}

} else {
String[] filterFileds = transPropertyNameToFieldNames(clz, fullName, selectedFields);
Set<String> filerdFieldSet = new HashSet<>();
filerdFieldSet.addAll(Arrays.asList(filterFileds));
StringBuilder columnBuilder = new StringBuilder();
for (int i = 0; i < filterFileds.length; i++) {
if (filterFileds[i] != null) {
columnBuilder.append(filterFileds[i]).append(",");
if (filterFileds[i].equalsIgnoreCase(pkName)) {
hasPKField = true;
}
}
}
columnBuilder.append("pubts").append(",");
columnBuilder.append("dr");
if (!hasPKField) {
columnBuilder.append(",").append(pkName);
}
StringBuilder sql = new StringBuilder();
sql.append("SELECT ").append(columnBuilder).append(" FROM ").append(tableName).append(" WHERE ").append(condition);
BaseProcessor processor = new BeanListProcessor(baseEntity);
List<T> results = getJdbcSession().executeQuery(sql.toString(), param, processor);
if (results.size() >= 1) {
result = results.get(0);
}

//特征
for (OneToOneType oneToOneType : characteristicTypes) {
Class childClz = oneToOneType.getTargetEntity();
String childTable = BeanProcessorHelper.getTableName(childClz, oneToOneType.getTargetFullName());
String childSQL = buildSQLWithSelectedFields(oneToOneType, clz, fullName, selectedFields, resultMap, childTable);
if (StringUtils.isNotEmpty(childSQL)) {
characteristicEntityHandler(baseEntity, childSQL, fullName, result, ormAttributeSign);
}
}
//平行
for (OneToOneType oneToOneType : parallelTypes) {
Class childClz = oneToOneType.getTargetEntity();
String childTable = BeanProcessorHelper.getTableName(childClz, oneToOneType.getTargetFullName());
String childSQL = buildSQLWithSelectedFields(oneToOneType, clz, fullName, selectedFields, resultMap, childTable);
if (StringUtils.isNotEmpty(childSQL)) {
parallelEntityHandler(baseEntity, childSQL, childTable, result, ormAttributeSign);
}
}
}

}

return result;
}

com.yonyou.iuap.yms.imeta.MetaEntityHelper#getMetaEntity

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据fullName获取元数据信息
*
* @param fullName
* @return
*/
public static Entity getMetaEntity(String fullName) {
Entity entity = BizContext.getMetaRepository().entity(fullName);
if (entity == null) {
throw new IllegalArgumentException("can't get the meta info of" + fullName);
}
return entity;
}

BizContext.getMetaRepository()得到的是空指针,因此出现异常

解决方案

询问张健

  • 找到yms-jdbc-biz模块
  • 找到src/test/java/imetatest/MetaEntityHelperTest.java
  • 找到com.yonyou.iuap.yms.imeta.MetaEntityHelper
  • 将前者的类文件内容拷贝至后者中即可

YMS控制台配置数据源(诗涵解决)

image-20230209150237331

编写接口queryByUnionKeyMap实现联合查询

需求

public T queryByUnionKeyMap(BaseEntity baseEntity, Map<String,Object> unionKeyMap, String[] selectedFields) throws BusinessException;

实现:

select name,id from table where id=1;

map.put(“id”,1)
map.put(“ytenant_id”,1)
map.put(“dr”,1)
select name,id from table where id=1 and ytenant_id=aaa and dr=1

代码

IYmsJdbcApi#queryByUnionKeyMap

com.yonyou.iuap.yms.api.IYmsJdbcApi#queryByUnionKeyMap

1
2
3
4
5
6
7
8
9
10
/**
* 根据Map查询数据,只返回指定字段的数据
* @param baseEntity
* @param unionKeyMap
* @param selectedFields
* @param <T>
* @return
* @throws BusinessException
*/
public <T> T queryByUnionKeyMap(BaseEntity baseEntity, Map<String,Object> unionKeyMap, String[] selectedFields) throws BusinessException;

AbstractDAO#queryByUnionKeyMap&queryByUnionKeyMap

com.yonyou.iuap.yms.dao.AbstractDAO#queryByUnionKeyMap(com.yonyou.iuap.yms.param.BaseEntity, java.util.Map<java.lang.String,java.lang.Object>)以及com.yonyou.iuap.yms.dao.AbstractDAO#queryByUnionKeyMap(com.yonyou.iuap.yms.param.BaseEntity, java.util.Map<java.lang.String,java.lang.Object>, java.lang.String[])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 根据主键列表查询数据,查询指定VO
* @param baseEntity
* @param unionKeyMap
* @param <T>
* @return
* @throws BusinessException
*/
public <T> T queryByUnionKeyMap(BaseEntity baseEntity, Map<String, Object> unionKeyMap) throws BusinessException {
return queryByUnionKeyMap(baseEntity, unionKeyMap, null);
}

/**
* 根据主键列表查询数据,返回指定列的VO对象
* @param baseEntity
* @param unionKeyMap
* @param selectedFields
* @param <T>
* @return
* @throws BusinessException
*/
@Override
public <T> T queryByUnionKeyMap(BaseEntity baseEntity, Map<String, Object> unionKeyMap, String[] selectedFields) throws BusinessException {
try {
manager = createPersistenceManager();
return (T) manager.retrieveByUnionKeyMap(baseEntity, unionKeyMap, selectedFields);
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw new DAOException(e.getMessage());
}
}

AbstractJdbcPersistenceManager#retrieveByPK&retrieveByUnionKeyMap

com.yonyou.iuap.yms.jdbc.AbstractJdbcPersistenceManager#retrieveByPK(com.yonyou.iuap.yms.param.BaseEntity, ID, java.lang.String[])以及com.yonyou.iuap.yms.jdbc.AbstractJdbcPersistenceManager#retrieveByUnionKeyMap(com.yonyou.iuap.yms.param.BaseEntity, java.util.Map<java.lang.String,java.lang.Object>)

修改原方法retrieveByPK(),添加现方法retrieveByUnionKeyMap()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
/**
* 根据主键查询
*/
public <T, ID> T retrieveByPK(BaseEntity baseEntity, ID pk) throws Exception {
return retrieveByPK(baseEntity, pk, null);
}

/**
* 根据主键查询
*/
public <T, ID> T retrieveByPK(BaseEntity baseEntity, ID pk, String[] selectedFields) throws Exception {
if (pk == null) {
throw new IllegalArgumentException("pk is null");
}
T result = null;

Map<String, Object> unionKeyMap = new HashMap<>();
unionKeyMap.put(BeanProcessorHelper.getPkColumn(baseEntity.getClass(), baseEntity.getFullName()), pk);
//直接调用联合查询方法
result = retrieveByUnionKeyMap(baseEntity, unionKeyMap, selectedFields);

return result;
}


/**
* 联合查询,返回指定字段数据
*/
public <T> T retrieveByUnionKeyMap(BaseEntity baseEntity, Map<String, Object> unionKeyMap) throws Exception {
return retrieveByUnionKeyMap(baseEntity, unionKeyMap, null);
}


/**
* 联合查询,返回指定字段数据
*/
public <T> T retrieveByUnionKeyMap(BaseEntity baseEntity, Map<String, Object> unionKeyMap, String[] selectedFields) throws Exception {
//先判断传入的unionKeyMap是否为空,如果为空返回
if (unionKeyMap == null) {
throw new IllegalArgumentException("unionKeyMap is null"); //pk改为unionKeyMap
}
T result = null;
boolean ormAttributeSign = BeanDataOperateHelper.initOrmAttributeSign(baseEntity);
Map<String, Object> resultMap = null;
SQLParameter param = new SQLParameter();

//遍历Map的Key和Value,Key加入到临时conditionList中,Value加入到param.paramList中
List<String> conditionList = new ArrayList<>();
for(Map.Entry<String, Object> entry : unionKeyMap.entrySet()){
conditionList.add(entry.getKey());
param.addParam(entry.getValue());
}

Class clz = baseEntity.getClass();
String fullName = baseEntity.getFullName();
Map<String, ORMColumnInfo> types = findColumnInfo(clz, fullName);
Entity entity = MetaEntityHelper.getMetaEntity(fullName);
List<Property> elasticAttributes = entity.getElasticAttributes();

//buildSQL
for (Property elasticProperty : elasticAttributes) {
types.remove(elasticProperty.columnName());
}
StringBuffer columnsForQuery = new StringBuffer();
for (String columnName : types.keySet()) {
columnsForQuery.append(columnName).append(",");
}
//删掉最后一个逗号
columnsForQuery.delete(columnsForQuery.length() - 1, columnsForQuery.length());

List<OneToOneType> parallelTypes = BizMetaHelper.getOneToOneField(baseEntity, null, null);
List<OneToOneType> characteristicTypes = BeanProcessorHelper.getCharacteristicOneToOneField(fullName);
//改前:condition仅包括主键
//String condition = BeanProcessorHelper.getPkColumn(baseEntity.getClass(), baseEntity.getFullName()) + "=?";
//改后:condition包括unionKeyMap中的所有Key值
StringBuffer condition = new StringBuffer();
for(String s : conditionList){
condition.append(s + "=? and ");
}
condition.delete(condition.length() - 5, condition.length());

if (CollectionUtils.isEmpty(parallelTypes) && CollectionUtils.isEmpty(characteristicTypes)) {
String pkName = BeanProcessorHelper.getPkColumn(clz, fullName);
boolean hasPKField = false;
StringBuffer mainSQL = new StringBuffer();
String tableName = BeanProcessorHelper.getTableName(clz, fullName);
if (selectedFields == null) {
mainSQL.append("SELECT ").append(columnsForQuery).append(" FROM ").append(tableName);
} else {
selectedFields = transPropertyNameToFieldNames(clz, fullName, selectedFields);
String[] columns = getValidNames(fullName, clz, types, baseEntity, selectedFields);
mainSQL.append("SELECT ");
for (int i = 0; i < columns.length; i++) {
if (columns[i] != null) {
mainSQL.append(columns[i]).append(",");
if (columns[i].equalsIgnoreCase(pkName)) {
hasPKField = true;
}
}
}
if (!hasPKField) {
mainSQL.append(pkName).append(",");
}
mainSQL.setLength(mainSQL.length() - 1);
mainSQL.append(" FROM ").append(tableName);
}
mainSQL.append(" WHERE ").append(condition);
BaseProcessor processor = new BeanListProcessor(baseEntity);
List<T> results = getJdbcSession().executeQuery(mainSQL.toString(), param, processor);
if (results.size() >= 1) {
result = results.get(0);
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("query for oneToOne .. {} ", clz.getClasses());
}
Field[] fields = baseEntity.getClass().getDeclaredFields();
String[] propNames = new String[fields.length];
for (int i = 0; i < fields.length; i++) {
propNames[i] = fields[i].getName();
}
String pkName = BeanProcessorHelper.getPkColumn(clz, fullName);
boolean hasPKField = false;
StringBuffer mainSQL = new StringBuffer();
String tableName = BeanProcessorHelper.getTableName(clz, fullName);
StringBuilder mapSQL = new StringBuilder();
mapSQL.append("SELECT * FROM ").append(tableName).append(" WHERE ").append(condition);
List<Map> mapList = getJdbcSession().executeQuery(mapSQL.toString(), param, new MapListProcessor());
if (mapList.size() >= 1) {
resultMap = mapList.get(0);
} else {
logger.debug("查询结果为空!");
return null;
}
if (selectedFields == null) {
mainSQL.append("SELECT ").append(columnsForQuery).append(" FROM ").append(tableName).append(" WHERE ").append(condition);
List<T> results = getJdbcSession().executeQuery(mainSQL.toString(), param, new BeanListProcessor(baseEntity));
if (results.size() >= 1) {
result = results.get(0);
}
//特征
for (OneToOneType oneToOneType : characteristicTypes) {
String childSQL = buildSQLWithoutSelectFields(oneToOneType, clz, fullName, resultMap);
if (StringUtils.isNotEmpty(childSQL)) {
characteristicEntityHandler(baseEntity, childSQL, fullName, result, ormAttributeSign);
}
}
//平行
for (OneToOneType oneToOneType : parallelTypes) {
String childTable = BeanProcessorHelper.getTableName(oneToOneType.getTargetEntity(), oneToOneType.getTargetFullName());
String childSQL = buildSQLWithoutSelectFields(oneToOneType, clz, fullName, resultMap);
if (StringUtils.isNotEmpty(childSQL)) {
parallelEntityHandler(baseEntity, childSQL, childTable, result, ormAttributeSign);
}
}

} else {
String[] filterFileds = transPropertyNameToFieldNames(clz, fullName, selectedFields);
Set<String> filerdFieldSet = new HashSet<>();
filerdFieldSet.addAll(Arrays.asList(filterFileds));
StringBuilder columnBuilder = new StringBuilder();
for (int i = 0; i < filterFileds.length; i++) {
if (filterFileds[i] != null) {
columnBuilder.append(filterFileds[i]).append(",");
if (filterFileds[i].equalsIgnoreCase(pkName)) {
hasPKField = true;
}
}
}
columnBuilder.append("pubts").append(",");
columnBuilder.append("dr");
if (!hasPKField) {
columnBuilder.append(",").append(pkName);
}
StringBuilder sql = new StringBuilder();
sql.append("SELECT ").append(columnBuilder).append(" FROM ").append(tableName).append(" WHERE ").append(condition);
BaseProcessor processor = new BeanListProcessor(baseEntity);
List<T> results = getJdbcSession().executeQuery(sql.toString(), param, processor);
if (results.size() >= 1) {
result = results.get(0);
}

//特征
for (OneToOneType oneToOneType : characteristicTypes) {
Class childClz = oneToOneType.getTargetEntity();
String childTable = BeanProcessorHelper.getTableName(childClz, oneToOneType.getTargetFullName());
String childSQL = buildSQLWithSelectedFields(oneToOneType, clz, fullName, selectedFields, resultMap, childTable);
if (StringUtils.isNotEmpty(childSQL)) {
characteristicEntityHandler(baseEntity, childSQL, fullName, result, ormAttributeSign);
}
}
//平行
for (OneToOneType oneToOneType : parallelTypes) {
Class childClz = oneToOneType.getTargetEntity();
String childTable = BeanProcessorHelper.getTableName(childClz, oneToOneType.getTargetFullName());
String childSQL = buildSQLWithSelectedFields(oneToOneType, clz, fullName, selectedFields, resultMap, childTable);
if (StringUtils.isNotEmpty(childSQL)) {
parallelEntityHandler(baseEntity, childSQL, childTable, result, ormAttributeSign);
}
}
}

}

return result;
}

上传git

develop_wjc分支

image-20230213162630405

image-20230213162720140

MDD框架学习

任务

https://bip-daily.yyuap.com/#/
用户名:19908188888
密码: test0630

验证码666666

从图书订单TCC开始看,跟代码进行逻辑梳理

  • 首先是进入页面

image-20230202170945908

image-20230202171010198

  • 查看构建

image-20230202171110864

image-20230202171203111

可以对应上开发和实际页面,点击配置后进行代码跟进

order–>stock–>pay

  • 工程间调用关系梳理

image-20230201111246717

  • 工程中,分别学习

image-20230202171333291

image-20230202171348768

image-20230202171404751

看的过程中,结合mdd工程代码,进行学习

采用double shift进行查找yts相关进行学习,结合yts评审文档中mdd和yts结合过程,梳理yts在mdd中的作用,结合方式等

image-20230202171448043

学习笔记

总览

全局:

image-20230214151116644

上方:

image-20230214150755125

左侧:

image-20230214151429919

右侧为:

image-20230214151002135

下左侧为:

下右侧为:

image-20230214151053561

配置保存流程–跟进代码.md

页面操作及规则查看

image-20230203100154865

对应

image-20230203100239463

查看保存过程

image-20230203100319880

规则详细内容

com.yonyou.ucf.mdd.ext.bill.rule.biz.FillPKRule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Service("fillPKRule")
public class FillPKRule extends AbstractCommonRule {
public FillPKRule() {
}

public RuleExecuteResult execute(BillContext billContext, Map<String, Object> paramMap) throws Exception {
//列表查询dto:查询条件、分页、汇总显示
BillDataDto billData = (BillDataDto)this.getParam(paramMap);

List<BizObject> bills = this.getBills(billContext, paramMap);
FillFkDao.execute(billContext.getFullname(), bills);
return new RuleExecuteResult();
}
}

### com.yonyou.ucf.mdd.ext.dao.meta.biz.FillFkDao

public static <T extends BizObject> void execute(String fullname, List<T> bills) throws Exception {
initIPKGeneratorConfig();
Map<String, KeyIterator> insertEntityPK = ipkGeneratorConfig.configMap();


Entity entity=MetaDaoHelper.getEntity(fullname);
// 遍历所有对象
for (BizObject bill : bills) {

ObjectHierarchyBuilder.build(bill, entity);
// 设置主键
//20200722 只支持number 类型 ObjectPKWalker pkWalker = new ObjectPKWalker(insertEntityPK);
MddObjectPKWalker pkWalker = new MddObjectPKWalker(insertEntityPK);
ObjectWalker.walk(pkWalker, bill, fullname);

// 设置关联关系字段值(主子实体中,子对象外键)
ObjectFKWalker fkWalker = new ObjectFKWalker();
// 在一次遍历中通过next可以设置多个walker
// ChainWalker<Association, BizObject> otherWalker = new ...;
// fkWalker.setNext(otherWalker);
ObjectWalker.walk(fkWalker, bill, fullname);
}

}
com.yonyou.ucf.mdd.ext.bill.rule.check.CheckUniqueRule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
@Service("checkUniqueRule")
public class CheckUniqueRule extends AbstractCommonRule {
private static final Logger log = LoggerFactory.getLogger(CheckUniqueRule.class);

public CheckUniqueRule() {
}

public RuleExecuteResult execute(BillContext billContext, Map<String, Object> paramMap) throws Exception {
if (billContext == null) {
return new RuleExecuteResult();
} else {
List<BizObject> bills = this.getBills(billContext, paramMap);
if (null != bills && bills.size() != 0) {
Entity entity = MetaDaoHelper.getEntity(billContext.getFullname());
if (null == entity) {
throw new Exception("查询元数据失败:" + billContext.getFullname());
} else {
ServiceSupportHolder holder = new ServiceSupportHolder(billContext, paramMap, bills, entity);
String config = this.getConfig();
holder.setConfig(config);
BillDataDto billDataDto = (BillDataDto)this.getParam(paramMap);
holder.setBillData(billDataDto);
IServiceSupportApi iServiceSupportApi = (IServiceSupportApi)AppContext.getBean(CheckUniqueSupportService.class);
return (RuleExecuteResult)iServiceSupportApi.execute(holder);
}
} else {
return new RuleExecuteResult();
}
}
}
}


### com.yonyou.ucf.mdd.support.abs.AbstractServiceSupport
public Object execute(ServiceSupportHolder holder) throws Exception {
beforeHandler(holder); //空
Object resultHandler = doExecute(holder);
afterExecute(holder, resultHandler); //空
return resultHandler;
}

### com.yonyou.ucf.mdd.support.support.CheckUniqueSupportService
@Override
protected Object doExecute(ServiceSupportHolder holder) throws Exception {
IDoBillService iDoBillService = getBeanDo(ServiceSupportStatic.CheckUnique.DO);
return iDoBillService.doExecute(holder);
}

### com.yonyou.ucf.mdd.support.support.check.checkunique.DoExecuteCheckUniqueService
@Override
public Object doExecute(ServiceSupportHolder holder) throws Exception {
BillDataDto item = holder.getBillData();
//是否check具体某个属性比如check_name,check_code
boolean isHasItem = null == item.getItem() ? false : true;
if (isHasItem) {
CheckItem checkItem = new Gson().fromJson(item.getItem(), CheckItem.class);
if (null == checkItem || checkItem.getKey() == null) {
return new RuleExecuteResult();
}
}
Entity entity = holder.getEntity();
//构建检查唯一性的BO对象
CheckUnionBO checkUnionBO = createCheckUnionBO(holder);
//待回滚的参数 并发唯一了
List<String> needRollList = new ArrayList<>();
List<BizObject> bills = holder.getBillList();
//是否支持内存check
try {
boolean autoCode = checkUnionBO.isAutoCode();
List<Map<String, Object>> codeTempDataCaches = new ArrayList<>();
// 自动编码下需要把编码换成UUID,防止并发时被唯一校验控制,在UpdateBillCodeRule由编码规则统一控制
if (!isHasItem && autoCode) {
codeTempDataCaches = checkUniqueCodeService.writeTempCode(bills, entity, holder.getBillContext(), holder.getParamMap());
}
//内存+redis检查
checkLocalUniqueService.checkUnionKeyObJVM(holder.getBillContext(), entity, checkUnionBO, bills, needRollList);
//数据库检查 联合唯一性校验, 当校验参数为 null 用 is null 拼接条件
UniqueCheckWalker uniqueCheckWalker = new UniqueCheckWalker(AppContext.getPartitionContextData(entity, true), false);
if (!isHasItem) {
checkDbUniqueService.checkUnionWithOutItem(holder, bills, entity, checkUnionBO.isCheckChild(), uniqueCheckWalker);
if (autoCode) {
checkUniqueCodeService.reWriteCode(codeTempDataCaches, bills, autoCode);
}
} else {
checkDbUniqueService.checkUnionWithItem(holder.getBillContext(), bills, item, entity, uniqueCheckWalker);
}
} finally {
if (null != needRollList && needRollList.size() > 0) {
if (TransactionSynchronizationManager.isActualTransactionActive()) {
TransactionSynchronizationManager.registerSynchronization(new DoExecuteAfterTransactionCompletion(needRollList));
}
}
}
return new RuleExecuteResult();
}
com.yonyou.ucf.mdd.ext.bill.rule.crud.SaveBillRule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
@Service("saveBillRule")
public class SaveBillRule extends AbstractCommonRule {
private static final Logger log = LoggerFactory.getLogger(SaveBillRule.class);

public SaveBillRule() {
}

@ResubmitAnnotations
public RuleExecuteResult execute(BillContext billContext, Map<String, Object> paramMap) throws Exception {
return this.executeCommon(billContext, paramMap);
}

public RuleExecuteResult executeCommon(BillContext billContext, Map<String, Object> paramMap) throws Exception {
List<BizObject> bills = this.getBills(billContext, paramMap);
Entity entity = StringUtils.isBlank(billContext.getFullname()) ? null : IMetaUtils.entity(billContext.getFullname());
ServiceSupportHolder holder = new ServiceSupportHolder(billContext, paramMap, bills, entity);
holder.setcBillNo(billContext.getBillnum());
this.extendEnhance(holder);
IServiceSupportApi iServiceSupportApi = (IServiceSupportApi)AppContext.getBean(SaveBillSupportService.class);
return (RuleExecuteResult)iServiceSupportApi.execute(holder);
}

protected void extendEnhance(ServiceSupportHolder holder) {
holder.getAttribute().setAttr("saveRealAutoCode", false);
}
}


### com.yonyou.ucf.mdd.support.abs.AbstractServiceSupport
public Object execute(ServiceSupportHolder holder) throws Exception {
beforeHandler(holder);
Object resultHandler = doExecute(holder);
afterExecute(holder, resultHandler);
return resultHandler;
}

protected abstract Object doExecute(ServiceSupportHolder holder) throws Exception;


### com.yonyou.ucf.mdd.support.support.SaveBillSupportService
@Override
protected Object doExecute(ServiceSupportHolder holder) throws Exception {
IDoBillService iDoBillService = getBeanDo(ServiceSupportStatic.Save.DO);
return iDoBillService.doExecute(holder);
}

### com.yonyou.ucf.mdd.support.support.save.DoExecuteSaveBillService
/**
*
* @param holder
* @return
* @throws Exception
*
* @author Seraph
* @date 2021/5/2710:57 PM
*/
@Override
public Object doExecute(ServiceSupportHolder holder) throws Exception {
List<BizObject> bills = holder.getBillList();
for (BizObject bill : bills) {
checkRelevantRuleVerify(holder,bill);
executeOneBill(holder, bill);
}
return new RuleExecuteResult();
}

/**
*
* @param holder
* @param bill
* @throws Exception
*
* @author Seraph
* @date 2021/5/2710:57 PM
*/
@Override
public void executeOneBill(ServiceSupportHolder holder, BizObject bill) throws Exception {
BillContext billContext = holder.getBillContext();
boolean isInsert = EntityStatus.Insert == bill.getEntityStatus() ? true : false;
setAudit(isInsert, bill, holder.getEntity().fullname());
BarCodeSupport.generateBarCode(billContext, holder.getEntity(), bill);//处理条形码
setStatusInfoWhenInsert(holder,Status.newopen, bill, isInsert); //设置单据状态
dealSupportBpm(bill, billContext, isInsert);//处理工作流
String code = getAndDealCode(holder, billContext, bill, isInsert); //获取处置编码规则

//保存业务逻辑
Object result = saveBillServiceImpl.executeSave(billContext, bill, holder.getParamMap(), holder.getEntity(), isInsert);

//重置编码规则
if (null != code) {
bill.set("code", code);
}

BillSaveResult billSaveResult = (BillSaveResult) result;
String partitionWhereCondition = saveBillServiceImpl.buildQueryWhere4UpdateTree(holder.getEntity(), billContext); //构建扩展分词条件
updateTree(bill, billContext.getFullname(), AppContext.getTenantId(), billSaveResult, partitionWhereCondition); //更新树
// sendTimeLineSnapshot(billContext, bill, holder.getParamMap(), isInsert); //新增时上传时间轴快照
}


### com.yonyou.ucf.mdd.ext.bill.service.SaveBillServiceImpl
/**
* @see SaveBillServiceImpl#executeSave(com.yonyou.ucf.mdd.ext.model.BillContext, org.imeta.orm.base.BizObject, java.util.Map, org.imeta.core.model.Entity, java.lang.Boolean)
* @see InsertSqlExecutor#execute(org.imeta.core.model.Entity, java.util.List)
* @see UpdateSqlExecutor#execute(org.imeta.core.model.Entity, java.util.List)
*
* <p>
* execute insert or update with _statis value <code>String</code>
* </p>
*
* @param billContext billContext is one thread context {@link BillContext} in the variable has props context {@link Map} <code>null</code>save Resubmit key and other So.
* * fullname : metaUri of metaServer is the index of entity of meta
* @param bill business data in {@link com.yonyou.ucf.mdd.ext.bill.dto.BillDataDto}
* @param paramMap Singleton instance in rule china {@link Map} ,the request DTO in paramMap when in rule ,the DTO is instance of {@link BizObject} Array
* * when complete this rule set paramMap for return
* @param entity meta entity in metaServer
* @param isInert _statis value <code>String</code> insert or update
*
* @return execute result
* @throws Exception
*/
@Override
public Object executeSave(BillContext billContext, BizObject bill, Map<String, Object> paramMap, Entity entity, Boolean isInert) throws Exception {
try {
if (isInert) {
return executeInsert(billContext, bill, paramMap, entity);
}
return executeUpdate(billContext, bill, entity);
}catch (MddZeroResultMsgException zr){
throw new MddVouchStateMsgException(MsgExceptionCode.BILL_CHANGED_PLEASE_REFRESH);
}
}

/**
* @see SaveBillServiceImpl#executeSave(com.yonyou.ucf.mdd.ext.model.BillContext, org.imeta.orm.base.BizObject, java.util.Map, org.imeta.core.model.Entity, java.lang.Boolean)
* @see InsertSqlExecutor#execute(org.imeta.core.model.Entity, java.util.List)
*
* <p>
* execute insert or update with _statis value <code>String</code>
* </p>
*
* @param billContext billContext is one thread context {@link BillContext} in the variable has props context {@link Map} <code>null</code>save Resubmit key and other So.
* * fullname : metaUri of metaServer is the index of entity of meta
* @param bill business data in {@link com.yonyou.ucf.mdd.ext.bill.dto.BillDataDto}
* @param paramMap Singleton instance in rule china {@link Map} ,the request DTO in paramMap when in rule ,the DTO is instance of {@link BizObject} Array
* * when complete this rule set paramMap for return
* @param entity meta entity in metaServer
*
* @return execute result
* @throws Exception
*/
private Object executeInsert(BillContext billContext, BizObject bill, Map<String, Object> paramMap, Entity entity) throws Exception {
fillPubts4Insert(bill);
//开始递归插入数据
new InsertSqlExecutor(AppContext.getSqlSession()).execute(entity, bill);
//check project is open config of timeline and this bill is opened in timeline server
//sendTimeLineSnapshot(billContext, bill, paramMap);
return new BillSaveResult();
}


### com.yonyou.ucf.mdd.ext.dao.meta.crud.InsertSqlExecutor
/**
* 元数据crud封装的insert实现
*
* @author Bob
*/
@SuppressWarnings("DuplicatedCode")
public class InsertSqlExecutor extends MetaDaoSupport {

...

public <T extends BizObject> void execute(Entity entity, T data) throws Exception {
List<T> list = new ArrayList<T>(1);
list.add(data);
execute(entity, list, true);
}

private <T extends BizObject> void execute(Entity entity, List<T> list, boolean isPartition) throws Exception {
if (true) {//强制切换到InsertSqlExecutorNew
new InsertSqlExecutorNew(getSqlSession()).execute(entity, list);
return;
}
printLogWHenEntityNull(entity, list);
//传递多语开关配置到元数据上下文
String fullname = entity.fullname();
MddMultilingualUtil.setEnableI18n2Imeta();
//提前将insert插入,为yTenant 判断做准备
EntityStatusWalker statusWalker = new EntityStatusWalker();
for (BizObject bill : list) {
bill.setEntityStatus(EntityStatus.Insert);
ObjectWalker.walk(statusWalker, bill, fullname);
}
if (isPartition) {
setTenantandCorpWhenSave(entity, list);
}

InsertCheckWalker insertCheck = new InsertCheckWalker();
insertCheck.setNext(new FormatCheckWalker());
// 构造SQL语句
SqlBuilder<BizObject> builder = new InsertSqlBuilder();
for (BizObject bill : list) {
ObjectHierarchyBuilder.build(bill, entity);
ObjectFullWalker.walk(insertCheck, bill, fullname);
try {
batchSaveExecute(builder.build(entity, bill));
} catch (Throwable throwable) {
logger.error(throwable.getMessage());
throw new MddOrmErrorException(throwable.getMessage(),throwable);
}
}
}
com.yonyou.ucf.mdd.ext.pub.rule.SaveBusinessLogRule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Order(2147483647)
@Service("saveBusinessLogRule")
public class SaveBusinessLogRule extends AbstractCommonRule {
private static final Logger logger = LoggerFactory.getLogger(SaveBusinessLogRule.class);
private ExecutorService taskExecutor;
private AuditLogSender auditLogSender;

public SaveBusinessLogRule() {
}

public RuleExecuteResult execute(final BillContext billContext, final Map<String, Object> paramMap) {
final List<BizObject> bills = this.getCloneBills(billContext, paramMap);
final String token = InvocationInfoProxy.getYhtAccessToken();
final String clientIp = (String)InvocationInfoProxy.getExtendAttribute("clientIp");
final String serviceCode = (String)InvocationInfoProxy.getExtendAttribute("serviceCode");
if (StringUtils.isBlank(serviceCode)) {
try {
BillDataDto billDataDto = (BillDataDto)this.getParam(paramMap);
if (null != billDataDto) {
Map<String, Object> externalData = BillUtils.getExternalData(billDataDto);
serviceCode = (String)externalData.get("serviceCode");
}
} catch (Exception var10) {
if (logger.isErrorEnabled()) {
logger.error("从BillDataDto扩展参数中获取serviceCode失败");
}
}
}

if (!CollectionUtils.isEmpty(bills)) {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
public void afterCompletion(int status) {
if (0 == status) {
SaveBusinessLogRule.this.getTaskExecutor().execute(() -> {
SaveBusinessLogRule.this.executeAsync(billContext, paramMap, token, clientIp, bills, serviceCode);
});
}

}
});
} else {
logger.warn("业务日志异步执行,未开启事务。");
this.getTaskExecutor().execute(() -> {
this.executeAsync(billContext, paramMap, token, clientIp, bills, serviceCode);
});
}
} else {
JsonFormatter formatter = new JsonFormatter(BizContext.getMetaRepository());
String json = formatter.toJson(bills, billContext.getFullname(), false, 32).toString();
logger.error("业务日志getCloneBills为空: 参数---billContext:{},bills:{}", JSONObject.toJSONString(billContext), json);
}

return new RuleExecuteResult();
}
com.yonyou.ucf.mdd.ext.bill.rule.biz.RefreshTsRule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Service("refreshTsRule")
public class RefreshTsRule extends AbstractCommonRule {
public RefreshTsRule() {
}

public RuleExecuteResult execute(BillContext billContext, Map<String, Object> paramMap) throws Exception {
String fullname = billContext.getFullname();
List<BizObject> bills = this.getBills(billContext, paramMap);
String refreshField = (String)this.getParam(paramMap, "refreshField");
if (null != bills && bills.size() > 0) {
Iterator var6 = bills.iterator();

while(var6.hasNext()) {
BizObject bill = (BizObject)var6.next();
BillInfoUtils.refreshTs(fullname, bill, refreshField, AppContext.getTenantId());
DataCleanWalker dataCleanWalker = new DataCleanWalker();
dataCleanWalker.setNext(new ParallelTableWalker());
ObjectFullWalker.walk(dataCleanWalker, bill, fullname);
}

LogicDeleteHelper.removeLogicDelete(fullname, bills);
if (bills.size() == 1) {
this.putParam(paramMap, "return", bills.get(0));
} else {
this.putParam(paramMap, "return", bills);
}
}

return new RuleExecuteResult();
}
}
com.yonyou.common.bizflow.rule.BizFlowWriteBackRule低代码回写规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public RuleExecuteResult execute(BillContext billContext, Map<String, Object> paramMap) throws Exception {
RuleExecuteResult result = new RuleExecuteResult();
LogUtil.log("业务流:BizFlowWriteBackRule start! ");

try {
this.doWriteBack(billContext, paramMap);
LogUtil.log("业务流:BizFlowWriteBackRule end! ");
} catch (Exception var5) {
LogUtil.log("业务流:BizFlowWriteBackRule error!", var5);
}

return result;
}

private void doWriteBack(BillContext billContext, Map<String, Object> paramMap) throws Exception {
BillDataDto reqDto = (BillDataDto)paramMap.get("param");
String domain = BizFlowUtil.getDomain(billContext);
String billNum = BizFlowUtil.getBillNum(reqDto);
String tenantId = BizFlowUtil.getTenantId();
String token = InvocationInfoProxy.getYhtAccessToken();
String operate = billContext.getAction();
String subId = billContext.getSubid();
List<JSONObject> bills = BizFlowUtil.getBills(reqDto);
Iterator var11 = bills.iterator();

while(var11.hasNext()) {
JSONObject bill = (JSONObject)var11.next();
if (this.noNeedWriteBackConvert(bill, domain, operate)) {
LogUtil.log("业务流:BizFlowWriteBackRule skip bill: " + bill);
} else {
LogUtil.log("业务流:BizFlowWriteBackRule bill: " + bill);
ConvertParam convertParam = new ConvertParam();
convertParam.setTenantId(tenantId);
convertParam.setBillNum(billNum);
convertParam.setBillId(bill.getString("id"));
convertParam.addBill(bill);
convertParam.setToken(token);
convertParam.setOperate(operate);
convertParam.setSubId(subId);
convertParam.setDomain(domain);
LogUtil.log("业务流:BizFlowWriteBackRule convert PARAM: " + JSON.toJSONString(convertParam));
ConvertResult convertResult = BizFlowUtil.doWriteBackConvert(convertParam);
LogUtil.log("业务流:BizFlowWriteBackRule convert result: " + JSON.toJSONString(convertResult));
}
}

}

private boolean noNeedWriteBackConvert(JSONObject bill, String domain, String action) {
if (!"audit".equalsIgnoreCase(action) && !"unaudit".equalsIgnoreCase(action)) {
String flowId = ObjectUtil.getString(bill, "bizFlowId");
if ("developplatform".equalsIgnoreCase(domain) && ObjectUtil.isEmpty(flowId)) {
return true;
} else {
String writeBackSign = ObjectUtil.getString(bill, "bizFlowWriteBackSign");
return "bizFlowWriteBackSign".equals(writeBackSign);
}
} else {
return false;
}
}
com.yonyou.common.bizflow.rule.BizFlowPushRule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static final Logger log = LoggerFactory.getLogger(BizFlowPushRule.class);
@Autowired
IYpdBillDataService iYpdBillDataService;
@Autowired
MdfTemplateRunTimeServiceImpl mdfTemplateRunTimeService;

public BizFlowPushRule() {
}

public RuleExecuteResult execute(BillContext billContext, Map<String, Object> paramMap) throws Exception {
BizFlowRuleResult result = new BizFlowRuleResult();
LogUtil.log("业务流:bizFlowPush start! ");

try {
BillDataDto reqDto = (BillDataDto)paramMap.get("param");
Map<String, Object> cusMap = reqDto.getCustMap();
String billNum = BizFlowUtil.removeSuffix(reqDto.getBillnum());
List<JSONObject> bills = BizFlowUtil.getBills(reqDto);
String domain = BizFlowUtil.getDomain(billContext);
ConvertResult convertResult = BizFlowUtil.doPush(billNum, bills, cusMap, billContext.getSubid(), domain, billContext.getAction());
if (Boolean.TRUE.equals(AppContext.getCurrentUser().getNewArch())) {
this.busObjToUIData(billContext, paramMap, convertResult);
}

paramMap.put("bizFlowReturn", convertResult);
result.setConvertResult(convertResult);
LogUtil.log("业务流:bizFlowPush end! ");
} catch (Exception var10) {
LogUtil.log("业务流:bizFlowPush error!", var10);
}

return result;
}

image-20230203141336848

com.yonyou.business.StockServiceTccRule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
@Component("stockServiceTccRule")
public class StockSer viceTccRule extends AbstractCommonRule implements ITccRule {
// private static final String URI = "XMFYGJ102.XMFYGJ102.bookstock_yts";
//日常
private static final String URI = "GT52146AT26.GT52146AT26.bookstock_yts";

private static final String PRODUCTID = "sproductid";
private static final String ITOTAL = "itotal";
private static final String INUMBER = "inumber";
private static final String IONWAY = "ionway";
private static final String SNAME = "sname";

protected final static Logger logger = LoggerFactory.getLogger(StockServiceTccRule.class);

@Autowired
IOrderService orderService;

@Autowired
private RedisTemplate redisTemplate;

@Override
public RuleExecuteResult execute(BillContext billContext, Map<String, Object> map) throws Exception {
logger.info("StockServiceRule running!");
List<BizObject> bills = this.getBills(billContext, map);
boolean hasTccFlag = false;
for (BizObject bizObject : bills) {
String name = bizObject.get(SNAME);
if (!StringUtils.isEmpty(name) && name.toLowerCase(Locale.ENGLISH).indexOf("iris") != -1) {
hasTccFlag = true;
}
}
for (BizObject bizObject : bills) {
String ProductId = bizObject.get(PRODUCTID);
if (null == ProductId) {
throw new RuntimeException("商品ID不能为空");
}
// 下单数量
Integer num = bizObject.get(INUMBER);
if (null == num || num <= 0) {
throw new RuntimeException("数量不能为空或小于0");
}
BizObject stock = MetaDaoHelper.findById(URI, ProductId);
stock.put("_status", EntityStatus.Update);
// tcc模式,正向先扣库存
Integer oldItotal = (Integer)stock.get(ITOTAL);
if (num > oldItotal) {
throw new RuntimeException("库存不足");
}
stock.put(ITOTAL, oldItotal - num);
// 增加在途库存
Integer oldIonway = (Integer)stock.get(IONWAY);
stock.put(IONWAY, oldIonway + num);

Transaction yts = YtsContext.currentTransaction();
if (null != yts) {
stock.put("gtxid", yts.getGtxId());
stock.put("ptxid", yts.getPtxId());
stock.put("txid", yts.getTxId());
}

//将sorderid存入redis
String sorderid = bizObject.get("sorderid");
redisTemplate.opsForValue().set(sorderid,yts.getGtxId(),30l, TimeUnit.MINUTES);

MetaDaoHelper.update(URI, stock);

}
if (hasTccFlag) {
TourOrder order = new TourOrder();
order.setUserId(UUID.randomUUID().toString());
order.setTourOrderId(UUID.randomUUID().toString());
order.setOrderName("订单号:" + order.getOrderName());
order.setUserName("yongyou");

TourOrder norder = orderService.sagas(order);
try {
logger.info("order: {}", JSON.toJSONString(norder));
} catch (Throwable e) {
logger.warn("toJSONString error ", e);
}
}
return new RuleExecuteResult();
}

@Override
public RuleExecuteResult confirm(BillContext billContext, Map<String, Object> paramMap) throws Exception {
logger.info("StockServiceRule rollback!");
List<BizObject> bills = this.getBills(billContext, paramMap);
for (BizObject bizObject : bills) {
String ProductId = bizObject.get(PRODUCTID);
if (null == ProductId) {
throw new RuntimeException("商品ID不能为空");
}
// 下单数量
Integer num = bizObject.get(INUMBER);
if (null == num || num <= 0) {
throw new RuntimeException("数量不能为空或小于0");
}
// tcc模式成功,减去在途库存,交易完成
BizObject stock = MetaDaoHelper.findById(URI, ProductId);
stock.put("_status", EntityStatus.Update);
Integer oldIonway = (Integer)stock.get(IONWAY);
stock.put(IONWAY, oldIonway - num);

MetaDaoHelper.update(URI, stock);
}
return new RuleExecuteResult();
}

@Override
public RuleExecuteResult cancel(BillContext billContext, Map<String, Object> map) throws Exception {
logger.info("StockServiceRule rollback!");
List<BizObject> bills = this.getBills(billContext, map);
for (BizObject bizObject : bills) {
String ProductId = bizObject.get(PRODUCTID);
if (null == ProductId) {
throw new RuntimeException("商品ID不能为空");
}
// 下单数量
Integer num = bizObject.get(INUMBER);
if (null == num || num <= 0) {
throw new RuntimeException("数量不能为空或小于0");
}
// tcc模式失败,将在途库存加回可用库存
BizObject stock = MetaDaoHelper.findById(URI, ProductId);
stock.put("_status", EntityStatus.Update);
Integer oldItotal = (Integer)stock.get(ITOTAL);
stock.put(ITOTAL, oldItotal + num);

Integer oldIonway = (Integer)stock.get(IONWAY);
stock.put(IONWAY, oldIonway - num);

MetaDaoHelper.update(URI, stock);
}
return new RuleExecuteResult();
}

}
com.yonyou.business.PayServiceTccRule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
@Component("payServiceTccRule")
public class PayServiceTccRule extends AbstractCommonRule implements ITccRule {
// private static final String URI = "XMFYGJ103.XMFYGJ103.bookpay_yts";
//日常
private static final String URI = "GT52131AT25.GT52131AT25.bookpay_yts";

private static final String SUSERID = "sbuyer";
private static final String FTOTAL = "ftotal";
private static final String FAVAILABLE = "favailable";
private static final String FONWAY = "fonway";
private static final String SNAME = "sname";


@Autowired
IOrderService orderService;

protected final static Logger logger = LoggerFactory.getLogger(PayServiceTccRule.class);

@Override
public RuleExecuteResult execute(BillContext billContext, Map<String, Object> map) throws Exception {
logger.info("PayServiceRule running!");
List<BizObject> bills = this.getBills(billContext, map);
boolean hasTccFlag = false;
for (BizObject bizObject : bills) {
String name = bizObject.get(SNAME);
if (!StringUtils.isEmpty(name) && name.toLowerCase(Locale.ENGLISH).indexOf("tcc") != -1) {
hasTccFlag = true;
}
}
/*if(hasTccFlag) {
TourOrder order = new TourOrder();
order.setUserId(UUID.randomUUID().toString());
order.setTourOrderId(UUID.randomUUID().toString());
order.setOrderName("订单号:" + order.getOrderName());
order.setUserName("yongyou");
orderService.tcc(order);
}*/

for (BizObject bizObject : bills) {
// 账户
String UserId = bizObject.get(SUSERID);
if (null == UserId) {
throw new RuntimeException("账户ID不能为空");
}
// 待支付金额
BigDecimal total = bizObject.get(FTOTAL);
if (null == total || total.floatValue() <= 0) {
throw new RuntimeException("支付金额不能为空或小于0");
}
BizObject pay = MetaDaoHelper.findById(URI, UserId);
pay.put("_status", EntityStatus.Update);
// 账户可用金额
BigDecimal oldFavailable = pay.get(FAVAILABLE);
if (total.floatValue() > oldFavailable.floatValue()) {
throw new RuntimeException("当前账户余额不足");
}

pay.put(FAVAILABLE, oldFavailable.subtract(total));
// 账户在途金额,增加待付金额
BigDecimal oldOnway = pay.get(FONWAY);
pay.put(FONWAY, oldOnway.add(total));

Transaction yts = YtsContext.currentTransaction();
if (null != yts) {
pay.put("gtxid", yts.getGtxId());
pay.put("ptxid", yts.getPtxId());
pay.put("txid", yts.getTxId());
}
MetaDaoHelper.update(URI, pay);
}

return new RuleExecuteResult();
}

@Override
public RuleExecuteResult confirm(BillContext billContext, Map<String, Object> map) throws Exception {
logger.info("PayServiceRule rollback!");
List<BizObject> bills = this.getBills(billContext, map);
for (BizObject bizObject : bills) {
// 账户
String UserId = bizObject.get(SUSERID);
if (null == UserId) {
throw new RuntimeException("账户ID不能为空");
}
// 待支付金额
BigDecimal total = bizObject.get(FTOTAL);
if (null == total || total.floatValue() <= 0) {
throw new RuntimeException("支付金额不能为空或小于0");
}
BizObject pay = MetaDaoHelper.findById(URI, UserId);
pay.put("_status", EntityStatus.Update);

// 成功则把在途金额减掉,交易完成
BigDecimal oldOnway = pay.get(FONWAY);
pay.put(FONWAY, oldOnway.subtract(total));

MetaDaoHelper.update(URI, pay);
}
return new RuleExecuteResult();
}

@Override
public RuleExecuteResult cancel(BillContext billContext, Map<String, Object> map) throws Exception {
logger.info("PayServiceRule rollback!");
List<BizObject> bills = this.getBills(billContext, map);
for (BizObject bizObject : bills) {
// 账户
String UserId = bizObject.get(SUSERID);
if (null == UserId) {
throw new RuntimeException("账户ID不能为空");
}
// 待支付金额
BigDecimal total = bizObject.get(FTOTAL);
if (null == total || total.floatValue() <= 0) {
throw new RuntimeException("支付金额不能为空或小于0");
}
BizObject pay = MetaDaoHelper.findById(URI, UserId);
pay.put("_status", EntityStatus.Update);

// 失败则把在途金额加回可用金额
BigDecimal old = pay.get(FAVAILABLE);
pay.put(FAVAILABLE, old.add(total));

BigDecimal oldOnway = pay.get(FONWAY);
pay.put(FONWAY, oldOnway.subtract(total));

MetaDaoHelper.update(URI, pay);
}
return new RuleExecuteResult();
}

}

MDD框架com.yonyou.ucf.mdf.app.service.impl.BillServiceImp add方法梳理(部分内容).md

com.yonyou.ucf.mdf.app.service.impl.BillServiceImp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public String add(BaseReqDto addDto) throws Exception {
try {
//
RuleContext ruleContext = RuleEngineUtils.prepareRuleContext(addDto, OperationTypeEnum.ADD);
RuleExecuteResult result = RuleEngine.getInstance().execute(ruleContext);
if (result.getMsgCode() != 1) {
throw new BusinessException(result.getMessage());
} else {
return result.getData() == null ? "" : GsonHelper.ToJSon(result.getData());
}
} catch (Exception e) {
logger.error("bill add 异常",e);
throw new BusinessException(e.getMessage());
}
}

com/yonyou/ucf/mdd/rule/api/RuleEngine.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 规则引擎执行入口
*
* @param ruleContext 规则执行所需数据
* @return
*/
public RuleExecuteResult execute(RuleContext ruleContext) throws MddBaseException {
Queue<? extends RuleRegister> ruleList ;
try {
ruleList = doGetRules(ruleContext);
} catch (Throwable e) {
log.error("获取规则列表异常!", e);
throw new BusinessException(e.getMessage(), e);
}
try {
return doExecRules(ruleContext, ruleList);
} catch (Throwable e) {
log.error("执行rule异常!", e);
throw new BusinessException(e.getMessage(), e);
}
}

/**
* 调用handleRuleList 方法返回 规则列表
* 可适配自定义获取规则列表的 处理器。
* 自定义RuleListHandler 需实现 IRuleListHandler
*
* @param ruleContext
* @return
*/
public Queue<? extends RuleRegister> doGetRules(RuleContext ruleContext) throws Exception {
IRuleListHandler ruleListHandler = ruleContext.getRuleListHandler();
if (null == ruleListHandler) {
ruleListHandler = new DefaultRuleListHandler();
log.debug("##RuleEngine:doGetRules # 使用 DefaultRuleListHandler");
} else {
log.debug("##RuleEngine:doGetRules # 使用自定义 RuleListHandler");
}
Queue ruleList = ruleListHandler.handleRuleList(ruleContext);
ExtListUtil.checkRuleList(ruleList);
return ruleList;
}

com/yonyou/ucf/mdd/rule/handler/DefaultRuleListHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
@Override
public Queue<RuleRegister> handleRuleList(RuleContext ruleContext) throws Exception {

Object bill = ruleContext.getCustomMap().get("param");
String action = ruleContext.getOperateType() == null ? ruleContext.getOperationTypeEx() : ruleContext.getOperateType().getValue();
String[] ruleLvs = ruleContext.getRuleLvs();
if (ruleContext.isMakeup()) {//如果是异步规则,是否支持失败后规则补偿
return getMakeupRuleList(action, ruleContext.getTenantId());
}
Object uiTenantId = ruleContext.getTenantId();
if (ruleContext.getTenantId() == null || StringUtils.isEmpty(ruleContext.getTenantId() + "")) {
uiTenantId = AppContext.getTenantIdNoCallBack();
}
return getRuleList(ruleContext.getBillnum(), ruleLvs, action, bill, uiTenantId);
}



/**
* <p>
* important : action eq check and key is not null in billObject action set value check_key<code>null</code>
* getList should merge check with check_key
* </p>
*
* @param ruleLvs Rule Level :ruleLvs[0] = "common"; ruleLvs[1] = uiMetaBaseInfo.getSubid(); ruleLvs[2] = uiMetaBaseInfo.getBillnum();
* over rule : from high to low
* @param action the action for request exp: save,delete,update,list and so on
* @param bill object {@link BaseDto} if type of {@link BaseDto} return this else come from param others is <code>null</code>
* @param tenantId tenantId
* @return
* @throws Exception
*/
@SuppressWarnings({"rawtypes"})
private Queue<RuleRegister> getRuleList(String billnum, String[] ruleLvs, String action, Object bill, Object tenantId) throws Exception {
BaseDto billDto = getBaseDto(bill);
if (isCheck(action)) {
Map obj = new Gson().fromJson(billDto.getItem(), Map.class);
if (null != obj && obj.get("key") != null) {
action = "check_" + obj.get("key").toString();
}
}
String ruleKey = null;
if (null != billDto) {
if (null != billDto.getRuleKey()) {
ruleKey = billDto.getRuleKey();
} else {
if (StringUtils.equalsIgnoreCase(OperationTypeEnum.REFER.getValue(), action) || StringUtils.equalsIgnoreCase(action, OperationTypeEnum.REFERREFRESH.getValue())) {
ruleKey = billDto.getrefCode();
}
}
}
//获取可执行的redis队列
Queue<RuleRegister> ruleList = getAndFilterRules(billnum, action, ruleKey, tenantId, ruleLvs);
/* 设置依赖关系 */
dependOnRule(ruleList);
return ruleList;
}



/**
* 获取规则列表
* 根据ruleKey 规则关键字过滤
* 高级别规则对低级别规则的覆盖
*
* <p>
* 通过lvs循环查找规则
* 1)ruleLvs[2] = "common";
* 2)ruleLvs[1] = uiMetaBaseInfo.getSubid();
* 3)ruleLvs[0] = uiMetaBaseInfo.getBillnum();
* </p>
*
* @param action 动作
* @param ruleKey 规则关键字
* @param tenantId 租户ID
* @return
*/
public Queue<RuleRegister> getAndFilterRules(String billnum, String action, String ruleKey, Object tenantId, String[] ruleLvsOri) {
// 获取缓存, foreach 外,减少调用次数
List<RuleRegister> ruleRegisters;
if (RuleUtil.isOpenCache()) {//是否开启规则缓存,建议都开启,默认开启
ruleRegisters = RuleUtil.getBillNumRuleFromCache(tenantId, billnum, ruleLvsOri, action);
} else {
ruleRegisters = RuleUtil.initBillNumRuleRegister(tenantId, ruleLvsOri,action);
}
if(Tracker.switchEnable()){
Map<String, Object> inparam = new HashMap <>();
inparam.put("billnum", billnum);
inparam.put("action", action);
inparam.put("ruleKey", ruleKey);
inparam.put("tenantId", tenantId);
inparam.put("ruleLvsOri", ruleLvsOri);
Tracker.recordLogClues(this.getClass().getName(), "getAndFilterRules", inparam, ruleRegisters, "获取规则列表, 通过RuleUtil 获取的结果");
}

Queue<RuleRegister> ruleRegisterQueue = new LinkedList<>();
RuleStatus ruleStatus = new RuleStatus();
String[] ruleLvs = ruleLvsOri.clone();
//数组反转
ArrayUtils.reverse(ruleLvs);
for (int i = 0; i < ruleLvs.length; i++) {
String ruleLv = ruleLvs[i];
List<RuleRegister> tmpList = filterRules(ruleLv, ruleKey, ruleRegisters);
//增加同级别覆盖逻辑, 通过配置指定
overrideRule(tmpList, ruleRegisterQueue, ruleStatus, ruleLvs.length - i);
}
//队列排序
List<RuleRegister> list = new LinkedList(ruleRegisterQueue);
Collections.sort(list, RuleComparatorUtil.getInstance().getCmp());
ruleRegisterQueue.clear();
ruleRegisterQueue.addAll(list);
return ruleRegisterQueue;

}

com/yonyou/ucf/mdd/rule/utils/RuleUtil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 初始化表单基本的规则列表,如果是check_action,同步查询出check的规则列表
*
* @param tenantId 租户ID
* @param ruleLvsOri
* @return
*/
public static List<RuleRegister> initBillNumRuleRegister(Object tenantId, String[] ruleLvsOri,String ruleaction) {
Map<String, Object> params = new HashMap<>();
params.put("tenantId", tenantId);
List<String> list = new ArrayList<>();
for (String rule :ruleLvsOri){
if (StringUtils.isNotEmpty(rule)){
list.add(rule);
}
}
List<String> actionList = new ArrayList<>();
actionList.add(ruleaction);
if (ruleaction.startsWith("check_")){
list.add("check");
actionList.add("check");
}
params.put("list", list);
params.put("actionList", actionList);
List<RuleRegister> ruleRegisterList = findAllBillRuleRegister(params);
return ruleRegisterList;

}


/**
* 获取表单列表,可按照billnum进行过滤
*
* @param params
* @return
*/
private static List<RuleRegister> findAllBillRuleRegister(Map<String, Object> params) {
List<RuleRegister> ruleRegisterList = null;
try {
IRuleRegisterMapperDao iruledao = UBaseContext.getBean(IRuleRegisterMapperDao.class);
if (null == iruledao) {
iruledao = new RuleRegisterMapperDaoImpl();
}
ruleRegisterList = iruledao.findAllBillRuleRegister(params);
// 增加逻辑如果初始化当前租户没有规则,则使用公共规则 使用租户ID为0的
if (null == ruleRegisterList || ruleRegisterList.isEmpty()) {
params.put(MddConstants.PARAM_TENANT_ID, ExtCommonUtil.getZeroTenant());
ruleRegisterList = AppContext.getBean(IRuleRegisterMapperDao.class).findAllBillRuleRegister(params);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return ruleRegisterList;
}

SpringBoot项目创建

SpringBoot工程创建

image-20230115184814451

image-20230115185245172

pom.xml文件配置

持久化框架选择

个人使用mybatis plus框架

1
2
3
4
5
6
<!--    mybatis-plus   -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>

配置扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<build>
<!--根据文档添加配置扫描-->
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>

<!--plugins标签内容见下文-->
<plugins...>
</build>

application.properties文件配置

配置数据源(即数据库相关配置)

1
2
3
4
spring.datasource.url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
spring.datasource.username: root
spring.datasource.password: root
spring.datasource.driver-class-name: com.mysql.cj.jdbc.Driver

添加增删改查代码内容

以下内容作为参考

  • 项目结构

    user2结构及内容同user1

    image-20230115190137058

  • 代码示例

    添加一套简单的对数据库表增删改查逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    ## User1Controller.java

    /**
    * 实现一套简单的增删改查逻辑代码
    */
    @Slf4j
    @RestController
    @RequestMapping("/user1")
    public class User1Controller {

    @Autowired
    private User1Service user1Service;

    //新增用户1
    //增
    @PostMapping
    public String save(@RequestBody User1 user1){
    log.info("新增用户1,信息:{}", user1.toString());
    user1Service.save(user1);

    return "success";
    }

    //根据id查询用户
    //查
    @GetMapping("/{id}")
    public User1 get(@PathVariable Long id){
    User1 user1 = user1Service.getById(id);
    if(user1 != null){
    return user1;
    }else{
    log.info("查询失败");
    return null;
    }
    }

    //根据id更改用户信息
    //改
    @PutMapping
    public String update(@RequestBody User1 user1){
    log.info("更改用户信息:{}", user1.toString());
    user1Service.updateById(user1);

    return "success";
    }

    //根据id删除用户信息
    //删
    @DeleteMapping("/{id}")
    public String delete(@PathVariable Long id){
    user1Service.removeById(id);

    return "success";
    }

    }

    ## User1.java
    /**
    * 用户1信息
    */
    @Data
    public class User1 implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //姓名
    private String name;

    //手机号
    private String phone;

    //性别 0 女 1 男
    private String sex;

    //身份证号
    private String idNumber;

    //头像
    private String avatar;

    //状态 0:禁用,1:正常
    private Integer status;
    }

    ## User1Mapper.java
    @Mapper
    public interface User1Mapper extends BaseMapper<User1> {
    }

    ## User1ServiceImpl.java
    @Service
    public class User1ServiceImpl extends ServiceImpl<User1Mapper, User1> implements User1Service {
    }

    ## User1Service.java
    public interface User1Service extends IService<User1> {
    }

项目总体结构

image-20230115191613761

数据库建表sql

可使用Mysql可视化工具操作,选中本地test数据库

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `user1` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '姓名',
`phone` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '手机号',
`sex` varchar(2) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '性别',
`id_number` varchar(18) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '身份证号',
`avatar` varchar(500) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '头像',
`status` int DEFAULT '0' COMMENT '状态 0:禁用,1:正常',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='用户1信息'
1
2
3
4
5
6
7
8
9
10
CREATE TABLE `user2` (
`id` bigint NOT NULL COMMENT '主键',
`name` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '姓名',
`phone` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '手机号',
`sex` varchar(2) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '性别',
`id_number` varchar(18) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '身份证号',
`avatar` varchar(500) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '头像',
`status` int DEFAULT '0' COMMENT '状态 0:禁用,1:正常',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='用户2信息'

测试

此时可以使用Postman测试SpringBoot项目的增删改查功能,如下

image-20230115192530152

SpringBoot项目改为YMS项目

改造启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableAspectJAutoProxy
@EnableTransactionManagement(proxyTargetClass = true)
@MapperScan("com.example.demoyms.*.mapper")
public class DemoYmsApplication extends SpringBootServletInitializer {

public static void main(String[] args) {
// SpringApplication.run(DemoYmsApplication.class, args);
SpringApplication springApplication = new SpringApplicationBuilder(DemoYmsApplication.class).build();
springApplication.setAllowBeanDefinitionOverriding(true);
springApplication.run(args);
System.out.println("DemoYmsApplication启动完成...........");
}

}

改造pom.xml文件

配置统一依赖管理iuap-pom-with-ucf-parent

请参照YMS用户使用指南V0.2

根据我们的环境和需要选择一个iuap-pom-with-ucf-parent的版本,以下是iuap-pom-with-ucf-parent的版本以及对应的iuap-boot-starter、yms版本、流水线镜像对应版本如下:

image-20230115193703159

具体内容参照用户指南2.2.1

本示例工程使用版本如下:

1
2
3
4
5
<parent>
<groupId>com.yonyou.iuap</groupId>
<artifactId>iuap-2nd-party</artifactId>
<version>6.0.0-SNAPSHOT</version>
</parent>

业务工程配置iuap-boot-starter组件依赖

1
2
3
4
<dependency>
<groupId>com.yonyou.iuap.yms</groupId>
<artifactId>iuap-boot-starter</artifactId>
</dependency>

其他依赖

示例工程采用其他依赖

均为参考其他工程所得,可忽略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<dependency>
<artifactId>TwoCoordnate</artifactId>
<groupId>com.yonyou.cloud</groupId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>com.yonyou.cloud.middleware</groupId>
<artifactId>mwclient</artifactId>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.yonyou.cloud.middleware</groupId>
<artifactId>iris-spring-support</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>

打包插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<plugins>
<plugin>
<groupId>com.yonyou.iuap.yms</groupId>
<artifactId>yms-module-maven-plugin</artifactId>
<!-- 填写iuap-pom-with-ucf-parent里iuap-boot-starter对应的版本-->
<version>2.0.0-SNAPSHOT</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- 模块信息必须和spring.application.name的值一致-->
<moduleName>datasourceDemoWjc</moduleName>
<!-- 配置http/https访问该模块的根路径-->
<contextPath>/datasourceDemoWjc</contextPath>
<exteranlDependencies>
<dependency>
<artifactId>yms-external</artifactId>
<groupId>com.yonyou.cloud</groupId>
<!-- 与插件版本保持一致-->
<version>2.0.0-SNAPSHOT</version>
</dependency>
</exteranlDependencies>
<middleDependencies>
<dependency>
<artifactId>yms-middleware</artifactId>
<groupId>com.yonyou.cloud</groupId>
<!-- 与插件版本保持一致-->
<version>2.0.0-SNAPSHOT</version>
</dependency>
</middleDependencies>
</configuration>
</plugin>
</plugins>

application.properties文件配置

微服务配置 ak

1
2
3
4
access.key= IlJ0b5Di4xZuWnoX
access.secret= JqWjqpkSmManqVv53WIGDGo4kOggVn
app.domain=https://developer.yonyoucloud.com
portal.auth.url=${app.domain}/portal/api/v1/res/checkUserAuth

spring.application.name

微服务名称,后面配置YMS控制台会用到

环境标识

YMS是测试环境的话写test 开发环境写dev

1
spring.profiles.active=test

文件截图及注意事项

image-20230115194919568

注意:文件尽量使用.properties,不要使用.yml

代码上传至git

暂未操作

​ Git地址:http://git.yonyou.com

​ 用户名:用友域账号

​ 密码:用友域账号的密码

项目结构

image-20230115195217049

此后项目启动成功即可,成功截图未放

常见问题

parent引入失败

  • 开发前,请先根据相关文档配置maven–settings.xml文件

    参考文档,Jfrog私服开发配置流程,https://yundoc.yonyou.com/view/l/tjrxb14

  • 生成自己的settings.xml文件后,替换下图3中的路径文件

image-20230115195728465

parent和插件版本匹配问题

经过个人的尝试选择上述版本,过程中项目启动过程报错需要yms什么manager,回退了版本后不报错

项目启动过程遇到报错没有找到User1Mapper bean

经查找问题发现是包扫描路径出错

数据源组件创建

本地代码改造

创建配置类

1
2
3
4
5
6
7
8
9
10
@Slf4j
@Configuration
public class CustomDSConfiguration {

@Bean("master")
public DataSource masterDataSource(){
return new HikariDataSource();
}

}

关闭SpringBoot自动加载

1
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

此注解禁止 SpringBoot 自动注入数据源配置,关闭自动查找 application.yml 或者 properties 文件里的 spring.datasource.* 相关属性并自动配置单数据源

准备工作

准备YMS控制台账号

访问http://dc.yms.app.yyuap.com/confcenter/

账号密码请自行联系获得即可登录

获得YonBIP开发者平台使用权限

获取流程请参考以下操作:

  • 打开Windows端下的友空间平台

  • 进入工作台

  • 点击左上角图标

    image-20230115201002627

  • 搜索“审批”

  • 点击发起流程

    image-20230115201044626

  • 点击云平台开发者中心—>开发者中心使用权限—>发起审批即可

    image-20230115201335671

连接池创建

访问http://dc.yms.app.yyuap.com/confcenter/

流程

  • 根据2.2.3配置内容选择开发环境或测试环境

    image-20230115201703079

​ 其他环境请一定不要操作

  • image-20230115201832290

​ 点击添加连接池

  • 填写配置信息

    image-20230115202137342

    注意:连接池名称为自定义的可识别名称,连接池编码自定义可识别的名称,命名规范同java变量(参照YMS操作文档),用户名,密码,地址等请联系人员获得,YMS控制台与本地不通,请不要使用本地127.0.0.1数据库

    点击测试连接,通过后点击提交

  • 数据源配置成功

逻辑数据源创建及发布

创建流程

  • 选择测试环境—-点击配置管理—-点击任意产品名称—-点击逻辑数据源—-点击添加逻辑数据源

  • 配置逻辑数据源信息

    image-20230115203020550

    部分内容参照了YMS文档

​ 1.逻辑数据源名称,自定义的可识别名称

​ 2.逻辑数据源编码,逻辑数据源编码为规则为:spring.application.name+ “_” + 数据源的beanName,比如我的spring.application.name为datasourceDemoWjc,CustomDSConfiguration中注入的bean名称为master,则逻辑数据源编码为datasourceDemoWjc_master

​ 3.默认Schema,mysql为数据库的名称,其他有Schema分层的数据库为Schema。

​ 4.其他表单内容暂不确定

​ 5.点击提交

关联数据源连接池

  • 点击关联数据源连接池

image-20230115204110013

  • 选中3.3创建的连接池

image-20230115204214894

调试及常见问题

调试过程

  • 点击配置文件预览及发布

    image-20230115204603445

  • 将左侧的新值展开复制保存为yms_dynds_preload.json文件

    image-20230115204724362

  • 将该json文件内容删除最外侧的”datasource”层,如下图,否则启动时报错无法解析json

    image-20230115204914664

  • 使用postman进行测试,成功进行表数据增删改查

常见问题

数据源为空

通过代码设断点分析,得为json文件格式问题,最外层应该直接以logicDataSouceList等开始,而不能包一层datasouce,否则解析不到,分析过程如下:

  • com.yonyou.iuap.yms.datasource.YmsDataSourceConfiguration

    image-20230115205803701

  • com.yonyou.iuap.yms.datasource.ds.provider.impl.YMSDataSourceProvider

    image-20230115205840692

  • com.yonyou.iuap.yms.datasource.ds.provider.impl.YMSGlobalDataSourceProvider

    image-20230115205939666

  • com.yonyou.iuap.yms.datasource.service.DynamicDataSourceQryService

    image-20230115210010891

  • com.yonyou.iuap.yms.datasource.service.DisConfigLoadUtils

    image-20230115210048579

​ 在这里发现GsonUtils.fromJson传入参数content有yms_dynds.json解码内容,而返回的configDTO内容呢却全为空,那么通过比对YmsConfigDTO类的属性发现json文件不应该包含datasource层,故找到问题所在

启动报错sql异常,premises@10.8.32.101没有权限

由于10.8.32.101是我在家办公开启的vpn地址,我认为是没有权限的缘故

经排查确认其实是密码错误,yms_dynds.json文件中的密码由密文更换为非密文即可。

因此我将yms_dynds.json内容更换为本地环境进行测试,数据可以添加到连接池中配置的数据库,可以成功增删改查!

测试

本地项目启动后调用接口查看数据是否添加到连接池中配置的数据库

  • image-20230116103701245

    image-20230116103856638

  • image-20230116104053045

    image-20230116104124324

  • image-20230116104701605

    image-20230116104713323

  • image-20230116104746104

YTS文档

一致性框架(YTS)设计文档-评审-new

技术了解

分布式事务

本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

认识打桩

什么是桩

桩,或称桩代码,是指用来代替关联代码或者未实现代码的代码。如果函数B用B1来代替,那么,B称为原函数,B1称为桩函数。打桩就是编写或生成桩代码。

打桩的目的主要有:隔离补齐控制

隔离是指将测试任务从产品项目中分离出来,使之能够独立编译、链接,并独立运行。隔离的基本方法就是打桩,将测试任务之外的,并且与测试任务相关的代码,用桩来代替,从而实现分离测试任务。例如函数A调用了函数B,函数B又调用了函数C和D,如果函数B用桩来代替,函数A就可以完全割断与函数C和D的关系。

补齐是指用桩来代替未实现的代码,例如,函数A调用了函数B,而函数B由其他程序员编写,且未实现,那么,可以用桩来代替函数B,使函数A能够运行并测试。补齐在并行开发中很常用。

控制是指在测试时,人为设定相关代码的行为,使之符合测试需求。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
externint B();

int A()

{

int ret = B();

if(ret == 0)

;//do something

elseif(ret == 1)

;//do something

else

;//do something

return ret;

}

如果函数B返回随机数,或者返回网络状态,或者返回环境温度,等等,则当调用其实际代码时,函数A很难测试,这时可以用桩函数B1来代替B,使其返回测试所需要的数据。

Spring事务了解

https://blog.csdn.net/java123456111/article/details/124946361

事务、事务特性、

  • 编程式事务管理TransactionTemplate、TransactionManager

  • 声明式事务管理

    • 通过 AOP 实现(基于@Transactional 的全注解方式使用最多)

      1
      2
      3
      4
      5
      6
      7
      8
      @Transactional(propagation=propagation.PROPAGATION_REQUIRED)
      public void aMethod {
      //do something
      B b = new B();
      C c = new C();
      b.bMethod();
      c.cMethod();
      }
  • 2023/1/4,阅读未完成,1/5继续阅读

消息队列

消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。目前使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ

异步处理

引入消息队列,将不是必须的业务逻辑,异步处理。

image-20230104163020893

应用解耦

image-20230104163240498

流量削峰

image-20230104163357668

Iris

Iris是一款Go语言中用来开发web应用的框架,该框架支持编写一次并在任何地方以最小的机器功率运行,如Android、ios、Linux和Windows等。该框架只需要一个可执行的服务就可以在平台上运行了。

Iris框架以简单而强大的api而被开发者所熟悉。iris除了为开发者提供非常简单的访问方式外,还同样支持MVC。另外,用iris构建微服务也很容易。

在iris框架的官方网站上,被称为速度最快的Go后端开发框架。在Iris的网站文档上,列出了该框架具备的一些特点和框架特性,列举如下:

1)聚焦高性能

2)健壮的静态路由支持和通配符子域名支持

3)视图系统支持超过5以上模板

4)支持定制事件的高可扩展性Websocket API

5)带有GC, 内存 & redis 提供支持的会话

6)方便的中间件和插件

7)完整 REST API

8)能定制 HTTP 错误

9)源码改变后自动加载

Dubbo

image-20230104163812693

了解iuap

iuap聚焦PaaS、支撑SaaS、适配多种IaaS,遵循开放、开源、生态的发展原则,为企业提供互联网开发、测试、构造、发布、部署、云运维、云运营、云集成等各种平台技术能力,支撑企业构建高并发、高性能、高可用、安全的C2B、O2O、B2B 或B2B2C 等企业互联网应用或服务。

HttpClient

文档阅读

认知

本框架目前针对YonSuite(后续支持采购、财务、人力、协同等各领域云)如何保障各个微服务之间的数据一致性考虑

框架和iuap平台下的MDD、MDF及RPC结合,结合本地数据库事务提交机制,目前支持Mysql。后续逐步支持单纯RPC方式或者HTTP client方式。

技术中台的云端,提供事务监控中心,监控和查看未及时完成的及未正确结束的长事务,支持云端查看错误堆栈、链路拓扑图、下发重试命令等功能。

名词解释

image-20230104164808975

image-20230104164840000

image-20230104164850758

功能架构

image-20230104164925319

image-20230104165106147

技术架构设计

  • 需要再深入了解下事务协调器、事务管理器、运行时(TCC的try、confirm、cancel

  • 核心类和枚举

逻辑架构设计

核心业务场景

  • 销售出库信息回写(MDD)

image-20230104165531849

  • 财务支付业务凭证(RPC)

image-20230104165559017

  • 生态应用调用用友云开放服务(HTTP)

image-20230104165644171

领域概念模型

  • BASE理论模型

​ 通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态

  • 基于此的分布式一致性解决方案

    • Sagas模式
    • TCC模式
    • 异步Sagas模式
    • 基于MQ的可靠消息模式
  • 技术支撑设计

    • 基于Spring的DB事务回调

      image-20230104170219904

    • 分布式事务LOG及状态转换

      • Sagas正向操作的状态扭转
      • Tcc模式正向操作(Try)状态扭转
      • Tcc模式确认(Confirm)操作状态扭转
      • TCC,Sagas模式事务异常cancel操作的状态扭转
    • 序列化机制

      RPC调用上下文序列化采用Hession协议的二进制协议,hession在上下文包含java枚举以及泛型操作时能够正确处理。在RPC调用前,YTS的sdk将RPC调用的上下文信息,持久化到TransactionRel的数据表中,在确认(Confirm),回滚(Cancel)时从TransactionRel表中反序列化为RPC调用上下文进行RPC的调用。

    • 统一监控设计

    • 定时扫描数据任务

核心场景及流程

  • MDD框架正向业务调用
  • 正向调用报错后的业务回滚
  • 回滚报错后的错误信息上报
  • 云端重试命令的下发及执行
  • 分支事务嵌套链式长事务的协调执行
  • 分支事务嵌套链式长事务的逐级回滚

MDD(模型驱动开发)

传统的瀑布式开发流程如下图,每一个需求的产生都需要进行需求分析、设计、编码、测试等一系列流程。

image-20230104161534533

模型驱动框架的开发流程如下图:

image-20230104161552132

模型驱动的优势在于不用为每个需求定制化的编码,而是通过高度抽象化的模型映射到具体的业务元数据上来实现需求功能;这样大大减少了编码的重复工作,同时也提高了软件的可变更性。

模型驱动架构(MDA,Model Driven Architecture)

MDA的三大目标:轻便可移植性、互操作性和可重用性,采用了模型和技术分离的架构设计。

使用一定的建模标准(UML、MOF、XMI等)构建描述应用程序或集成系统的业务功能和行为的模型,这些模型独立于具体平台并且和实现具体业务功能和行为的特定技术代码分离,从而实现了业务和应用程序逻辑与底层平台技术的分离,这种分离也带来了应用程序的核心与技术变化周期的隔离。系统的业务部分和技术部分都可以各自进化而互不影响 - 业务逻辑响应业务需求,技术部分按业务需要利用新的技术开发。

image-20230104162015402

上图为MDA 结构示意图。最左侧为OMG提出的架构理论,其次是 MDA 的核心技术:MOF ( Meta Object Facility ,元对象设施)、 CWM (Common Warehouse Metamodel ,公共数据仓库元模型)和 UML . MDA 的主要工作就是要把基于这些技术建立的PIM 转换到不同的中间件平台上,得到对应的 PSM .PSM中给出的是目前主要针对的实现平台:CORBA 、 XML 、JAVA 、 Web Services 和 .NET .显然,随着技术的发展,这个列表将不断扩充。最后是基于PSM平台相关模型在公共服务及垂直领域等组件或应用中进行模型驱动开发。

领域驱动设计/模型驱动设计(DDD,Domain-Driven Design)

DDD是由Eric Evans最先提出,目的是对软件所涉及到的领域进行建模,以应对系统规模过大时引起的软件复杂性的问题。整个过程大概是这样的,开发团队和领域专家一起通过通用语言(Ubiquitous Language)去理解和消化领域知识,从领域知识中提取和划分为一个一个的子领域(核心子域,通用子域,支撑子域),并在子领域上建立模型,再重复以上步骤,这样周而复始,构建出一套符合当前领域的模型。

建模的过程是由不同阶段的成员来完成,有些模型之间有引用关系,应用软件通过所有人的建模工作而构建起来。

模型驱动开发(MDD, Model Driven Development)

模型驱动开发框架(MDF, MDD Framework)

MDF 实际上就是一套开发框架,可以是基于spring、spring boot 等技术开发框架搭建的脚手架,承载了模型驱动相关的设计、开发方法等的编码框架。

MDA环境下的系统开发方式就是在开发活动中通过创建各种模型精确描述不同的问题域,并利用模型转换来驱动包括分析、设计和实现等在内的整个软件开发过程。

不难看出MDA说的是整体的软件架构设计使用的是模型驱动;而在软件开发的过程中,对不同领域的业务需求进行分析、抽象建模和技术框架分层所常用的分析方法就是DDD;整个软件的开发活动就称之为MDD

image-20230104162435789

​ MDA架构设计中,MDA需要从需求采集和理解业务需求开始;PIM和PSM可以由不同团队完成,独立工作,但是组合后能产生健壮的业务解决方案;整个流程中模型转化和相关模型的驱动引擎是核心。

  接下来重点了解一下我们的MDD的脚手架(MDF)都做了什么?

  在脚手架中主要的是通过元数据SDK、规则SDK、UI元数据SDK和其他相关支持工具包完成对整体的模型驱动开发支持的。其中元数据SDK包含业务元数据的建模、数据查询等相关功能;UI元数据SDK包含了UI模板定义查询相关功能,搭配Node的前端驱动项目进行UI模板的渲染和组件的过滤展示等;而规则SDK包含了对于业务数据CRUD的默认规则、编码规则、唯一性校验规则、参照查询规则以及卡片翻页等默认规则。接下来让通过一个更完整的图来看MDD开发脚手架。

image-20230104162523785

​ 首先,需求输出到开发阶段,根据需求设计抽取定义业务元数据,及页面设计的UI元数据。

  其次,业务元数据通过XML或统一中央元数据仓库的形式加载到脚手架项目启动中。

  再次,浏览器访问node前端,node前端路由请求到java后端Controller.

  最后,后端处理逻辑在规则执行引擎中执行相应的规则,分别查询UI元数据用于页面渲染,以及业务数据的查询。并将结果返回给node前端做渲染展示。

  目前脚手架功能中部分扩展功能上图没有体现,这些扩展功能包括:

  a. UI元数据查询逻辑通过规则引擎查询,可通过增加前置或后置规则进行功能扩展;UI元数据支持远程获取;

  b. Redis 是否使用可通过开关配置。并支持单机模式,哨兵模式,集群模式配置;

  c. MySQL数据库支持规则、UI元数据和业务数据区分不同的数据源存储;

  d. 支持Ali OSS 存储;

  e. 支持基于ES的参照及翻译;

  f. 接入统一三方包管理和统一异常及日志;

  g. 支持MySQL、Redis、应用基本信息、环境信息等的健康检查与监控;

  以上就是MDD的基本实现原理和功能描述,整体看上去还是很简单清晰的。

总结展望

  下面通过下图表达一下模型驱动开发的优势

image-20230104162552160

继续阅读:https://blog.csdn.net/weixin_43145299/article/details/122623568

YMS文档

Linux基础-06-命令执行顺序控制与管道

命令控制顺序

当我们需要使用 apt-get 安装一个软件,然后安装完成后立即运行安装的软件或命令工具,又恰巧你的主机才更换的软件源还没有更新软件列表(比如之前我们的环境中,每次重新开始实验就得 sudo apt-get update,否则可能会报错提示 404),那么你可能会有如下一系列操作:

1
2
3
4
5
sudo apt-get update
# 等待执行完毕,然后输入下面的命令
sudo apt-get install some-tool # 这里 some-tool 需要替换成具体的软件包
# 等待安装完毕,然后输入软件包名称执行
some-tool

这时你可能就会想:要是我可以一次性输入完,让它自己去依次执行各命令就好了,这就是我们这一小节要解决的问题。

简单的顺序执行你可以使用 ; 来完成,比如上述操作你可以:

1
sudo apt-get update;sudo apt-get install some-tool;some-tool # 让它自己运行
1
which cowsay>/dev/null && cowsay -f head-in ohch~

你如果没有安装 cowsay,你可以先执行一次上述命令,你会发现什么也没发生,你再安装好之后你再执行一次上述命令,你也会发现一些惊喜。

上面的 && 就是用来实现选择性执行的,它表示如果前面的命令执行结果(不是表示终端输出的内容,而是表示命令执行状态的结果)返回 0 则执行后面的,否则不执行,你可以从 $? 环境变量获取上一次命令的返回结果:

1
which cowsay>/dev/null || echo "cowsay has not been install, please run 'sudo apt-get install cowsay' to install"

除了上述基本的使用之外,我们还可以结合着 &&|| 来实现一些操作,比如:

1
which cowsay>/dev/null && echo "exist" || echo "not exist"

我画个流程图来解释一下上面的流程:

image-20220825163901684

管道

管道是一种通信机制,通常用于进程间的通信(也可通过 socket 进行网络通信),它表现出来的形式就是将前面每一个进程的输出(stdout)直接作为下一个进程的输入(stdin)

管道又分为匿名管道和具名管道(这里将不会讨论在源程序中使用系统调用创建并使用管道的情况,它与命令行的管道在内核中实际都是采用相同的机制)。我们在使用一些过滤程序时经常会用到的就是匿名管道,在命令行中由 | 分隔符表示,| 在前面的内容中我们已经多次使用到了。具名管道简单的说就是有名字的管道,通常只会在源程序中用到具名管道。下面我们就将通过一些常用的可以使用管道的过滤程序来帮助你熟练管道的使用。

先试用一下管道,比如查看 /etc 目录下有哪些文件和目录,使用 ls 命令来查看:

1
ls -al /etc

有太多内容,屏幕不能完全显示,这时候可以使用滚动条或快捷键滚动窗口来查看。不过这时候可以使用管道:

1
ls -al /etc | less

通过管道将前一个命令(ls)的输出作为下一个命令(less)的输入,然后就可以一行一行地看。

cut命令

打印每一行的某一字段

打印 /etc/passwd 文件中以 : 为分隔符的第 1 个字段和第 6 个字段分别表示用户名和其家目录:

1
cut /etc/passwd -d ':' -f 1,6

打印 /etc/passwd 文件中每一行的前 N 个字符:

1
2
3
4
5
6
7
8
# 前五个(包含第五个)
cut /etc/passwd -c -5
# 前五个之后的(包含第五个)
cut /etc/passwd -c 5-
# 第五个
cut /etc/passwd -c 5
# 2 到 5 之间的(包含第五个)
cut /etc/passwd -c 2-5

grep命令

grep 命令是很强大的,也是相当常用的一个命令,它结合正则表达式可以实现很复杂却很高效的匹配和查找,不过在学习正则表达式之前,这里介绍它简单的使用,而关于正则表达式后面将会有单独一小节介绍到时会再继续学习 grep 命令和其他一些命令。

grep 命令的一般形式为:

1
grep [命令选项]... 用于匹配的表达式 [文件]...

还是先体验一下,我们搜索/home/shiyanlou目录下所有包含”shiyanlou”的文本文件,并显示出现在文本中的行号:

1
grep -rnI "shiyanlou" ~

image-20220825164541667

-r 参数表示递归搜索子目录中的文件,-n 表示打印匹配项行号,-I 表示忽略二进制文件。这个操作实际没有多大意义,但可以感受到 grep 命令的强大与实用。

当然也可以在匹配字段中使用正则表达式,下面简单的演示:

1
2
# 查看环境变量中以 "yanlou" 结尾的字符串
export | grep ".*yanlou$"

image-20220825164632617

其中$就表示一行的末尾

wc命令

wc 命令用于统计并输出一个文件中行、单词和字节的数目,比如输出 /etc/passwd 文件的统计信息:

1
wc /etc/passwd

分别只输出行数、单词数、字节数、字符数和输入文本中最长一行的字节数:

1
2
3
4
5
6
7
8
9
10
# 行数
wc -l /etc/passwd
# 单词数
wc -w /etc/passwd
# 字节数
wc -c /etc/passwd
# 字符数
wc -m /etc/passwd
# 最长行字节数
wc -L /etc/passwd

注意:对于西文字符来说,一个字符就是一个字节,但对于中文字符一个汉字是大于 2 个字节的,具体数目是由字符编码决定的。

再来结合管道来操作一下,下面统计 /etc 下面所有目录数:

1
ls -dl /etc/*/ | wc -l

image-20220825164745848

sort命令

这个命令前面我们也是用过多次,功能很简单就是将输入按照一定方式排序,然后再输出,它支持的排序有按字典排序,数字排序,按月份排序,随机排序,反转排序,指定特定字段进行排序等等。

默认为字典排序:

1
cat /etc/passwd | sort

反转排序:

1
cat /etc/passwd | sort -r

按特定字段排序:

1
cat /etc/passwd | sort -t':' -k 3

上面的-t参数用于指定字段的分隔符,这里是以”:”作为分隔符;-k 字段号用于指定对哪一个字段进行排序。这里/etc/passwd文件的第三个字段为数字,默认情况下是以字典序排序的,如果要按照数字排序就要加上-n参数:

1
cat /etc/passwd | sort -t':' -k 3 -n

注意观察第二个冒号后的数字:

uniq去重命令

uniq 命令可以用于过滤或者输出重复行。

  • 过滤重复行

我们可以使用 history 命令查看最近执行过的命令(实际为读取 ${SHELL}_history 文件,如我们环境中的 .zsh_history 文件),不过你可能只想查看使用了哪个命令而不需要知道具体干了什么,那么你可能就会要想去掉命令后面的参数然后去掉重复的命令:

1
history | cut -c 8- | cut -d ' ' -f 1 | uniq

然后经过层层过滤,你会发现确是只输出了执行的命令那一列,不过去重效果好像不明显,仔细看你会发现它确实去重了,只是不那么明显,之所以不明显是因为 uniq 命令只能去连续重复的行,不是全文去重,所以要达到预期效果,我们先排序:

1
2
3
history | cut -c 8- | cut -d ' ' -f 1 | sort | uniq
# 或者
history | cut -c 8- | cut -d ' ' -f 1 | sort -u

这就是 Linux/UNIX 哲学吸引人的地方,大繁至简,一个命令只干一件事却能干到最好。

  • 输出重复行
1
2
3
4
# 输出重复过的行(重复的只输出一个)及重复次数
history | cut -c 8- | cut -d ' ' -f 1 | sort | uniq -dc
# 输出所有重复的行
history | cut -c 8- | cut -d ' ' -f 1 | sort | uniq -D

文本处理命令还有很多,下一节将继续介绍一些常用的文本处理的命令。

Linux基础-05-帮助命令及定期任务

内建命令与外部命令

什么是内建命令,什么是外部命令呢?这和帮助命令又有什么关系呢?

因为有一些查看帮助的工具在内建命令与外建命令上是有区别对待的。

内建命令实际上是 shell 程序的一部分,其中包含的是一些比较简单的 Linux 系统命令,这些命令是写在 bash 源码的 builtins 里面的,由 shell 程序识别并在 shell 程序内部完成运行,通常在 Linux 系统加载运行时 shell 就被加载并驻留在系统内存中。而且解析内部命令 shell 不需要创建子进程,因此其执行速度比外部命令快。比如:history、cd、exit 等等。

外部命令是 Linux 系统中的实用程序部分,因为实用程序的功能通常都比较强大,所以其包含的程序量也会很大,在系统加载时并不随系统一起被加载到内存中,而是在需要时才将其调入内存。虽然其不包含在 shell 中,但是其命令执行过程是由 shell 程序控制的。外部命令是在 Bash 之外额外安装的,通常放在/bin,/usr/bin,/sbin,/usr/sbin 等等。比如:ls、vi 等。

简单来说就是:一个是天生自带的天赋技能,一个是后天得来的附加技能。我们可以使用 type 命令来区分命令是内建的还是外部的。例如这两个得出的结果是不同的

1
2
3
type exit

type vim

得到的是两种结果,若是对 ls 你还能得到第三种结果

image-20220825150935431

1
2
3
4
5
6
# 得到这样的结果说明是内建命令,正如上文所说内建命令都是在 bash 源码中的 builtins 的.def中
xxx is a shell builtin
# 得到这样的结果说明是外部命令,正如上文所说,外部命令在/usr/bin or /usr/sbin等等中
xxx is /usr/bin/xxx
# 若是得到alias的结果,说明该指令为命令别名所设定的名称;
xxx is an alias for xx --xxx

help命令

本实验环境是 zsh,而 zsh 中内置并没有 help 命令,我们可以进入 bash 中,在 bash 中内置有该命令

1
bash

做好了以上的准备,我们就可以愉快的使用 help 命令了,我们可以尝试下这个命令:

1
help ls

得到的结果如图所示,为什么是这样的结果?

image-20220825151258857

因为 help 命令是用于显示 shell 内建命令的简要帮助信息。帮助信息中显示有该命令的简要说明以及一些参数的使用以及说明,一定记住 help 命令只能用于显示内建命令的帮助信息,不然就会得到你刚刚得到的结果。

那如果是外部命令怎么办,不能就这么抛弃它呀。其实外部命令基本上都有一个参数 --help,这样就可以得到相应的帮助,看到你想要的东西了。试试下面这个命令是不是能看到你想要的东西了。

1
ls --help

man命令

你可以尝试下这个命令

1
man ls

得到的内容比用 help 更多更详细,而且 man 没有内建与外部命令的区分,因为 man 工具是显示系统手册页中的内容,也就是一本电子版的字典,这些内容大多数都是对命令的解释信息,还有一些相关的描述。通过查看系统文档中的 man 也可以得到程序的更多相关信息和 Linux 的更多特性。

是不是好用许多,当然也不代表 help 就没有存在的必要,当你非常紧急只是忘记该用哪个参数的时候,help 这种显示简单扼要的信息就特别实用,若是不太紧急的时候就可以用 man 这种详细描述的查询方式

在尝试上面这个命令时我们会发现最左上角显示“ LS (1)”,在这里,“ LS ”表示手册名称,而“(1)”表示该手册位于第一章节。这个章节又是什么?在 man 手册中一共有这么几个章节

章节数 说明
1 Standard commands (标准命令)
2 System calls (系统调用)
3 Library functions (库函数)
4 Special devices (设备说明)
5 File formats (文件格式)
6 Games and toys (游戏和娱乐)
7 Miscellaneous (杂项)
8 Administrative Commands (管理员命令)
9 其他(Linux 特定的), 用来存放内核例行程序的文档。

打开手册之后我们可以通过 pgup 与 pgdn 或者上下键来上下翻看,可以按 q 退出当前页面

info命令

要是你觉得 man 显示的信息都还不够,满足不了你的需求,那试试 info 命令,注意实验楼的环境中没有安装 info,可以手动安装,安装和操作步骤如下:

1
2
3
4
5
# 安装 info
sudo apt-get update
sudo apt-get install info
# 查看 ls 命令的 info
info ls

得到的信息是不是比 man 还要多了,info 来自自由软件基金会的 GNU 项目,是 GNU 的超文本帮助系统,能够更完整的显示出 GNU 信息。所以得到的信息当然更多

man 和 info 就像两个集合,它们有一个交集部分,但与 man 相比,info 工具可显示更完整的 GNU 工具信息。若 man 页包含的某个工具的概要信息在 info 中也有介绍,那么 man 页中会有“请参考 info 页更详细内容”的字样。

定期任务

crontab 命令

crontab 命令常见于 Unix 和类 Unix 的操作系统之中(Linux 就属于类 Unix 操作系统),用于设置周期性被执行的指令。

crontab 命令从输入设备读取指令,并将其存放于 crontab 文件中,以供之后读取和执行。通常,crontab 储存的指令被守护进程激活,crond 为其守护进程,crond 常常在后台运行,每一分钟会检查一次是否有预定的作业需要执行。

通过 crontab 命令,我们可以在固定的间隔时间执行指定的系统指令或 shell 脚本。时间间隔的单位可以是分钟、小时、日、月、周的任意组合。

这里我们看一看 crontab 的格式:

1
2
3
4
5
6
7
8
# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed

crontab 在本实验环境中需要做一些特殊的准备,首先我们会启动 rsyslog,以便我们可以通过日志中的信息来了解我们的任务是否真正的被执行了(在本实验环境中需要手动启动,而在自己本地中 Ubuntu 会默认自行启动不需要手动启动)。

1
2
sudo apt-get install -y rsyslog
sudo service rsyslog start

在本实验环境中 crontab 也是不被默认启动的,同时不能在后台由 upstart 来管理,所以需要我们来启动它:

1
sudo cron -f &

下面将开始 crontab 的使用了,我们通过下面一个命令来添加一个计划任务:

1
crontab -e

第一次启动会出现这样一个画面,这是让我们选择编辑的工具,选择第二个基本的 vim 就可以了。

image-20220825153119031

详细的格式可以使用上一节中学习到的 man 命令查看:

1
man crontab

在了解命令格式之后,我们通过这样的一个例子来完成一个任务的添加,在文档的最后一排加上这样一排命令,该任务是每分钟我们会在/home/shiyanlou 目录下创建一个以当前的年月日时分秒为名字的空白文件

1
*/1 * * * * touch /home/shiyanlou/$(date +\%Y\%m\%d\%H\%M\%S)

注意:

“ % ” 在 crontab 文件中,有结束命令行、换行、重定向的作用,前面加 ” \ ” 符号转义,否则,“ % ” 符号将执行其结束命令行或者换行的作用,并且其后的内容会被做为标准输入发送给前面的命令。

添加成功后我们会得到最后一排 installing new crontab 的一个提示:

image-20220825153152738

当然我们也可以通过这样的一个指令来查看我们添加了哪些任务:

1
crontab -l

通过图中的显示,我们也可以看出,我们正确的保存并且添加成功了该任务的:

image-20220825153207871

虽然我们添加了任务,但是如果 cron 的守护进程并没有启动,它根本都不会监测到有任务,当然也就不会帮我们执行,我们可以通过以下 2 种方式来确定我们的 cron 是否成功的在后台启动,默默的帮我们做事,若是没有就得执行上文准备中的第二步了。

1
2
3
4
5
ps aux | grep cron

# or

pgrep cron

通过下图可以看到任务在创建之后,执行了几次,生成了一些文件,且每分钟生成一个:

我们通过这样一个命令可以查看到执行任务命令之后在日志中的信息反馈:

1
sudo tail -f /var/log/syslog

从图中我们可以看到分别在 13 点 28、29、30 分的 01 秒为我们在 shiyanlou 用户的家目录下创建了文件。

当我们并不需要这个任务的时候我们可以使用这么一个命令去删除任务:

1
crontab -r

通过图中我们可以看出我们删除之后再查看任务列表,系统已经显示该用户并没有任务哦。

image-20220825153318309

深入

每个用户使用 crontab -e 添加计划任务,都会在 /var/spool/cron/crontabs 中添加一个该用户自己的任务文档,这样目的是为了隔离。

如果是系统级别的定时任务,需要 root 权限执行的任务应该怎么处理?

只需要使用 sudo 编辑 /etc/crontab 文件就可以。

cron 服务监测时间最小单位是分钟,所以 cron 会每分钟去读取一次 /etc/crontab/var/spool/cron/crontabs 里面的內容。

/etc 目录下,cron 相关的目录有下面几个:

image-20220825153412282

每个目录的作用:

  1. /etc/cron.daily,目录下的脚本会每天执行一次,在每天的 6 点 25 分时运行;
  2. /etc/cron.hourly,目录下的脚本会每个小时执行一次,在每小时的 17 分钟时运行;
  3. /etc/cron.monthly,目录下的脚本会每月执行一次,在每月 1 号的 6 点 52 分时运行;
  4. /etc/cron.weekly,目录下的脚本会每周执行一次,在每周第七天的 6 点 47 分时运行;

系统默认执行时间可以根据需求进行修改。

挑战:备份日志

image-20220825160033681

Linux基础-04-文件系统操作与磁盘管理

df, du, mount

查看磁盘容量和目录容量

  • 使用 df 命令查看磁盘的容量
1
df

物理主机上的 /dev/sda2 是对应着主机硬盘的分区,后面的数字表示分区号,数字前面的字母 a 表示第几块硬盘(也可能是可移动磁盘),你如果主机上有多块硬盘则可能还会出现 /dev/sdb/dev/sdc 这些磁盘设备都会在 /dev 目录下以文件的存在形式。

接着你还会看到”1k-块”这个陌生的东西,它表示以磁盘块大小的方式显示容量,后面为相应的以块大小表示的已用和可用容量,在你了解 Linux 的文件系统之前这个就先不管吧,我们以一种你应该看得懂的方式展示:

1
df -h

现在你就可以使用命令查看你主机磁盘的使用情况了。至于挂载点如果你还记得前面第 4 节介绍 Linux 目录树结构的内容,那么你就应该能很好的理解挂载的概念,这里就不再赘述。

  • 使用 du 命令查看目录的容量

这个命令前面其实已经用了很多次了:

1
2
3
4
# 默认同样以块的大小展示
du
# 加上 `-h` 参数,以更易读的方式展示
du -h

-d 参数指定查看目录的深度

1
2
3
4
# 只查看 1 级目录的信息
du -h -d 0 ~
# 查看 2 级
du -h -d 1 ~

常用参数

1
2
3
du -h # 同 --human-readable 以 K,M,G 为单位,提高信息的可读性。
du -a # 同 --all 显示目录中所有文件的大小。
du -s # 同 --summarize 仅显示总计,只列出最后加总的值。

虚拟磁盘等

dd 命令

简介

dd 命令用于转换和复制文件,不过它的复制不同于 cp。之前提到过关于 Linux 的很重要的一点,一切即文件,在 Linux 上,硬件的设备驱动(如硬盘)和特殊设备文件(如 /dev/zero/dev/random)都像普通文件一样,只是在各自的驱动程序中实现了对应的功能,dd 也可以读取文件或写入这些文件。这样,dd 也可以用在备份硬件的引导扇区、获取一定数量的随机数据或者空数据等任务中。dd 程序也可以在复制时处理数据,例如转换字节序、或在 ASCII 与 EBCDIC 编码间互换。

dd 的命令行语句与其他的 Linux 程序不同,因为它的命令行选项格式为 选项=值,而不是更标准的 –选项 值-选项=值dd 默认从标准输入中读取,并写入到标准输出中,但可以用选项 if(input file,输入文件)和 of(output file,输出文件)改变。

我们先来试试用 dd 命令从标准输入读入用户的输入到标准输出或者一个文件中:

1
2
3
4
5
# 输出到文件
dd of=test bs=10 count=1 # 或者 dd if=/dev/stdin of=test bs=10 count=1
# 输出到标准输出
dd if=/dev/stdin of=/dev/stdout bs=10 count=1
# 在打完了这个命令后,继续在终端打字,作为你的输入

上述命令从标准输入设备读入用户输入(缺省值,所以可省略)然后输出到 test 文件,bs(block size)用于指定块大小(缺省单位为 Byte,也可为其指定如 KMG 等单位),count 用于指定块数量。如上图所示,我指定只读取总共 10 个字节的数据,当我输入了 hello shiyanlou 之后加上空格回车总共 16 个字节(一个英文字符占一个字节)内容,显然超过了设定大小。使用 ducat 10 个字节(那个黑底百分号表示这里没有换行符),而其他的多余输入将被截取并保留在标准输入。

前面说到 dd 在拷贝的同时还可以实现数据转换,那下面就举一个简单的例子:将输出的英文字符转换为大写再写入文件:

1
dd if=/dev/stdin of=test bs=10 count=1 conv=ucase

你可以在man文档中查看其他所有转换参数。

使用 dd 命令创建虚拟镜像文件

通过上面一小节,你应该掌握了 dd 的基本使用,下面就来使用 dd 命令来完成创建虚拟磁盘的第一步。

/dev/zero 设备创建一个容量为 256M 的空文件:

1
2
dd if=/dev/zero of=virtual.img bs=1M count=256
du -h virtual.img

然后我们要将这个文件格式化(写入文件系统),这里我们要学到一个(准确的说是一组)新的命令来完成这个需求。

使用 mkfs 命令

格式化磁盘(我们这里是自己创建的虚拟磁盘镜像)

你可以在命令行输入 sudo mkfs 然后按下 <Tab> 键,你可以看到很多个以 mkfs 为前缀的命令,这些不同的后缀其实就是表示着不同的文件系统,可以用 mkfs 格式化成的文件系统。

我们可以简单的使用下面的命令来将我们的虚拟磁盘镜像格式化为 ext4 文件系统:

1
sudo mkfs.ext4 virtual.img

可以看到实际 mkfs.ext4 是使用 mke2fs 来完成格式化工作的。mke2fs 的参数很多,不过我们也不会经常格式化磁盘来玩,所以就掌握这基本用法吧,等你有特殊需求时,再查看 man 文档解决。

更多关于文件系统的知识,请查看 wiki: 文件系统 ext3ext4

如果你想知道 Linux 支持哪些文件系统你可以输入 ls -l /lib/modules/$(uname -r)/kernel/fs 查看(我们的环境中无法查看)。

使用 mount 命令

挂载磁盘到目录树

用户在 Linux/UNIX 的机器上打开一个文件以前,包含该文件的文件系统必须先进行挂载的动作,此时用户要对该文件系统执行 mount 的指令以进行挂载。该指令通常是使用在 USB 或其他可移除存储设备上,而根目录则需要始终保持挂载的状态。又因为 Linux/UNIX 文件系统可以对应一个文件而不一定要是硬件设备,所以可以挂载一个包含文件系统的文件到目录树。

Linux/UNIX 命令行的 mount 指令是告诉操作系统,对应的文件系统已经准备好,可以使用了,而该文件系统会对应到一个特定的点(称为挂载点)。挂载好的文件、目录、设备以及特殊文件即可提供用户使用。

我们先来使用 mount 来查看下主机已经挂载的文件系统:

1
sudo mount

输出的结果中每一行表示一个设备或虚拟设备,每一行最前面是设备名,然后是 on 后面是挂载点,type 后面表示文件系统类型,再后面是挂载选项(比如可以在挂载时设定以只读方式挂载等等)。

那么我们如何挂载真正的磁盘到目录树呢,mount 命令的一般格式如下:

1
mount [options] [source] [directory]

一些常用操作:

1
mount [-o [操作选项]] [-t 文件系统类型] [-w|--rw|--ro] [文件系统源] [挂载点]

注意:由于实验楼的环境限制,mount 命令挂载及 umount 卸载都无法进行操作,可以简单了解这些步骤。

现在直接来挂载我们创建的虚拟磁盘镜像到 /mnt 目录:

1
2
3
4
5
6
mount -o loop -t ext4 virtual.img /mnt
# 也可以省略挂载类型,很多时候 mount 会自动识别

# 以只读方式挂载
mount -o loop --ro virtual.img /mnt
# 或者 mount -o loop,ro virtual.img /mnt

使用 umount 命令卸载已挂载磁盘

注意:由于实验楼的环境限制,mount 命令挂载及 umount 卸载都无法进行操作,可以简单了解这些步骤。

1
2
# 命令格式 sudo umount 已挂载设备名或者挂载点,如:
sudo umount /mnt

不过遗憾的是,由于我们环境的问题(环境中使用的 Linux 内核在编译时没有添加对 Loop device 的支持),所以你将无法挂载成功

另外关于 loop 设备,你可能会有诸多疑问,那么请看下面来自维基百科 /dev/loop的说明:

使用 fdisk 为磁盘分区

(关于分区的一些概念不清楚的用户请参看 主引导记录

注意:由于实验楼的环境限制,fdisk 命令无法进行操作,可以简单了解这些步骤。

同样因为环境中没有物理磁盘,也无法创建虚拟磁盘的原因我们就无法实验练习使用该命令了,下面我将以我的物理主机为例讲解如何为磁盘分区。

1
2
# 查看硬盘分区表信息
sudo fdisk -l

image-20220825150314725

输出结果中开头显示了我主机上的磁盘的一些信息,包括容量扇区数,扇区大小,I/O 大小等信息。

我们重点看一下中间的分区信息,/dev/sda1/dev/sda2 为主分区分别安装了 Windows 和 Linux 操作系统,/dev/sda3 为交换分区(可以理解为虚拟内存),/dev/sda4 为扩展分区其中包含 /dev/sda5/dev/sda6/dev/sda7/dev/sda8 四个逻辑分区,因为主机上有几个分区之间有空隙,没有对齐边界扇区,所以分区之间不是完全连续的。

1
2
# 进入磁盘分区模式
sudo fdisk virtual.img

image-20220825150347651

在进行操作前我们首先应先规划好我们的分区方案,这里我将在使用 128M(可用 127M 左右)的虚拟磁盘镜像创建一个 30M 的主分区剩余部分为扩展分区包含 2 个大约 45M 的逻辑分区。

操作完成后输入 p 查看结果如下:

image-20220825150400564

最后不要忘记输入 w 写入分区表。

使用 losetup 命令

建立镜像与回环设备的关联

注意:由于实验楼的环境限制,losetup 命令无法进行操作,可以简单了解这些步骤。

同样因为环境原因中没有物理磁盘,也没有 loop device 的原因我们就无法实验练习使用该命令了,下面我将以我的物理主机为例讲解。

1
2
3
4
5
sudo losetup /dev/loop0 virtual.img
# 如果提示设备忙你也可以使用其它的回环设备,"ls /dev/loop*"参看所有回环设备

# 解除设备关联
sudo losetup -d /dev/loop0

然后再使用 mkfs 格式化各分区(前面我们是格式化整个虚拟磁盘镜像文件或磁盘),不过格式化之前,我们还要为各分区建立虚拟设备的映射,用到 kpartx 工具,需要先安装:

1
2
3
4
5
sudo apt-get install kpartx
sudo kpartx -av /dev/loop0

# 取消映射
sudo kpartx -dv /dev/loop0

image-20220825150421449

接着再是格式化,我们将其全部格式化为 ext4:

1
2
3
sudo mkfs.ext4 -q /dev/mapper/loop0p1
sudo mkfs.ext4 -q /dev/mapper/loop0p5
sudo mkfs.ext4 -q /dev/mapper/loop0p6

格式化完成后在 /media 目录下新建四个空目录用于挂载虚拟磁盘:

1
2
3
4
5
6
7
8
9
10
mkdir -p /media/virtualdisk_{1..3}
# 挂载磁盘分区
sudo mount /dev/mapper/loop0p1 /media/virtualdisk_1
sudo mount /dev/mapper/loop0p5 /media/virtualdisk_2
sudo mount /dev/mapper/loop0p6 /media/virtualdisk_3

# 卸载磁盘分区
sudo umount /dev/mapper/loop0p1
sudo umount /dev/mapper/loop0p5
sudo umount /dev/mapper/loop0p6

然后:

1
df -h

image-20220825150436515

Linux基础-03-文件打包与压缩

基本概念

在讲 Linux 上的压缩工具之前,有必要先了解一下常见常用的压缩包文件格式。在 Windows 上最常见的不外乎这两种 *.zip*.7z 后缀的压缩文件。而在 Linux 上面常见的格式除了以上两种外,还有 .rar*.gz*.xz*.bz2*.tar*.tar.gz*.tar.xz*.tar.bz2,简单介绍如下:

文件后缀名 说明
*.zip zip 程序打包压缩的文件
*.rar rar 程序压缩的文件
*.7z 7zip 程序压缩的文件
*.tar tar 程序打包,未压缩的文件
*.gz gzip 程序(GNU zip)压缩的文件
*.xz xz 程序压缩的文件
*.bz2 bzip2 程序压缩的文件
*.tar.gz tar 打包,gzip 程序压缩的文件
*.tar.xz tar 打包,xz 程序压缩的文件
*tar.bz2 tar 打包,bzip2 程序压缩的文件
*.tar.7z tar 打包,7z 程序压缩的文件

讲了这么多种压缩文件,这么多个命令,不过我们一般只需要掌握几个命令即可,包括 ziptar。下面会依次介绍这几个命令及对应的解压命令。

zip压缩打包

  • 使用 zip 打包文件夹,注意输入完整的参数和路径:
1
2
3
4
cd /home/shiyanlou
zip -r -q -o shiyanlou.zip /home/shiyanlou/Desktop
du -h shiyanlou.zip
file shiyanlou.zip

上面命令将目录 /home/shiyanlou/Desktop 打包成一个文件,并查看了打包后文件的大小和类型。第一行命令中,-r 参数表示递归打包包含子目录的全部内容,-q 参数表示为安静模式,即不向屏幕输出信息,-o,表示输出文件,需在其后紧跟打包输出文件名。后面使用 du 命令查看打包后文件的大小(后面会具体说明该命令)。

  • 设置压缩级别为 9 和 1(9 最大,1 最小),重新打包:
1
2
zip -r -9 -q -o shiyanlou_9.zip /home/shiyanlou/Desktop -x ~/*.zip
zip -r -1 -q -o shiyanlou_1.zip /home/shiyanlou/Desktop -x ~/*.zip

这里添加了一个参数用于设置压缩级别 -[1-9],1 表示最快压缩但体积大,9 表示体积最小但耗时最久。最后那个 -x 是为了排除我们上一次创建的 zip 文件,否则又会被打包进这一次的压缩文件中,注意:这里只能使用绝对路径,否则不起作用

我们再用 du 命令分别查看默认压缩级别、最低、最高压缩级别及未压缩的文件的大小:

1
du -h -d 0 *.zip ~ | sort

通过 man 手册可知:

  • -h, –human-readable(顾名思义,你可以试试不加的情况)
  • -d, –max-depth(所查看文件的深度)

image-20220824173730418

这样一目了然,理论上来说默认压缩级别应该是最高的,但是由于文件不大,这里的差异不明显(几乎看不出差别),不过你在环境中操作之后看到的压缩文件大小可能跟图上的有些不同,因为系统在使用过程中,会随时生成一些缓存文件在当前用户的家目录中,这对于我们学习命令使用来说,是无关紧要的,可以忽略这些不同。

  • 创建加密 zip 包

使用 -e 参数可以创建加密压缩包:

1
zip -r -e -o shiyanlou_encryption.zip /home/shiyanlou/Desktop

注意: 关于 zip 命令,因为 Windows 系统与 Linux/Unix 在文本文件格式上的一些兼容问题,比如换行符(为不可见字符),在 Windows 为 CR+LF(Carriage-Return+Line-Feed:回车加换行),而在 Linux/Unix 上为 LF(换行),所以如果在不加处理的情况下,在 Linux 上编辑的文本,在 Windows 系统上打开可能看起来是没有换行的。如果你想让你在 Linux 创建的 zip 压缩文件在 Windows 上解压后没有任何问题,那么你还需要对命令做一些修改:

1
zip -r -l -o shiyanlou.zip /home/shiyanlou/Desktop

需要加上 -l 参数将 LF 转换为 CR+LF 来达到以上目的。

unzip解压

shiyanlou.zip 解压到当前目录:

1
unzip shiyanlou.zip

使用安静模式,将文件解压到指定目录:

1
unzip -q shiyanlou.zip -d ziptest

上述指定目录不存在,将会自动创建。如果你不想解压只想查看压缩包的内容你可以使用 -l 参数:

1
unzip -l shiyanlou.zip

注意: 使用 unzip 解压文件时我们同样应该注意兼容问题,不过这里我们关心的不再是上面的问题,而是中文编码的问题,通常 Windows 系统上面创建的压缩文件,如果有有包含中文的文档或以中文作为文件名的文件时默认会采用 GBK 或其它编码,而 Linux 上面默认使用的是 UTF-8 编码,如果不加任何处理,直接解压的话可能会出现中文乱码的问题(有时候它会自动帮你处理),为了解决这个问题,我们可以在解压时指定编码类型。

使用 -O(英文字母,大写 o)参数指定编码类型:

1
unzip -O GBK 中文压缩文件.zip

tar打包工具

在 Linux 上面更常用的是 tar 工具,tar 原本只是一个打包工具,只是同时还是实现了对 7z、gzip、xz、bzip2 等工具的支持,这些压缩工具本身只能实现对文件或目录(单独压缩目录中的文件)的压缩,没有实现对文件的打包压缩,所以我们也无需再单独去学习其他几个工具,tar 的解压和压缩都是同一个命令,只需参数不同,使用比较方便。

下面先掌握 tar 命令一些基本的使用方式,即不进行压缩只是进行打包(创建归档文件)和解包的操作。

  • 创建一个 tar 包:
1
2
cd /home/shiyanlou
tar -P -cf shiyanlou.tar /home/shiyanlou/Desktop

上面命令中,-P 保留绝对路径符,-c 表示创建一个 tar 包文件,-f 用于指定创建的文件名,注意文件名必须紧跟在 -f 参数之后,比如不能写成 tar -fc shiyanlou.tar,可以写成 tar -f shiyanlou.tar -c ~。你还可以加上 -v 参数以可视的的方式输出打包的文件。

  • 解包一个文件(-x 参数)到指定路径的已存在目录(-C 参数):
1
2
mkdir tardir
tar -xf shiyanlou.tar -C tardir
  • 只查看不解包文件 -t 参数:
1
tar -tf shiyanlou.tar
  • 保留文件属性和跟随链接(符号链接或软链接),有时候我们使用 tar 备份文件当你在其他主机还原时希望保留文件的属性(-p 参数)和备份链接指向的源文件而不是链接本身(-h 参数):
1
tar -cphf etc.tar /etc

对于创建不同的压缩格式的文件,对于 tar 来说是相当简单的,需要的只是换一个参数,这里我们就以使用 gzip 工具创建 *.tar.gz 文件为例来说明。

  • 我们只需要在创建 tar 文件的基础上添加 -z 参数,使用 gzip 来压缩文件:
1
tar -czf shiyanlou.tar.gz /home/shiyanlou/Desktop
  • 解压 *.tar.gz 文件:
1
tar -xzf shiyanlou.tar.gz

image-20220824174403703

现在我们要使用其它的压缩工具创建或解压相应文件只需要更改一个参数即可:

压缩文件格式 参数
*.tar.gz -z
*.tar.xz -J
*tar.bz2 -j

tar 命令的参数很多,不过常用的就是上述这些,需要了解更多你可以查看 man 手册获取帮助。

总结

现在我们要使用其它的压缩工具创建或解压相应文件只需要更改一个参数即可:

压缩文件格式 参数
*.tar.gz -z
*.tar.xz -J
*tar.bz2 -j

tar 命令的参数很多,不过常用的就是上述这些,需要了解更多你可以查看 man 手册获取帮助。

Linux基础-02-文件及目录管理

一些小操作

快捷键

真正学习命令行之前,你先要掌握几个十分有用、必需掌握的小技巧:

[Tab]

使用Tab键来进行命令补全,Tab键一般是在字母Q旁边,这个技巧给你带来的最大的好处就是当你忘记某个命令的全称时可以只输入它的开头的一部分,然后按下Tab键就可以得到提示或者帮助完成:

[Ctrl+c]

强行终止当前程序

其他一些常用快捷键
按键 作用
Ctrl+d 键盘输入结束或退出终端
Ctrl+s 暂停当前程序,暂停后按下任意键恢复运行
Ctrl+z 将当前程序放到后台运行,恢复到前台为命令fg
Ctrl+a 将光标移至输入行头,相当于Home
Ctrl+e 将光标移至输入行末,相当于End
Ctrl+k 删除从光标所在位置到行末
Alt+Backspace 向前删除一个单词
Shift+PgUp 将终端显示向上滚动
Shift+PgDn 将终端显示向下滚动

学会使用通配符

通配符是一种特殊语句,主要有星号(*)和问号(?),用来对字符串进行模糊匹配(比如文件名、参数名)。当查找文件夹时,可以使用它来代替一个或多个真正字符;当不知道真正字符或者懒得输入完整名字时,常常使用通配符代替一个或多个真正字符。

Shell 常用通配符:

字符 含义
* 匹配 0 或多个字符
? 匹配任意一个字符
[list] 匹配 list 中的任意单一字符
[^list] 匹配 除 list 中的任意单一字符以外的字符
[c1-c2] 匹配 c1-c2 中的任意单一字符 如:[0-9][a-z]
{string1,string2,...} 匹配 string1 或 string2 (或更多)其一字符串
{c1..c2} 匹配 c1-c2 中全部字符 如{1..10}

帮助

使用man命令,它是Manual pages的缩写。

通常情况下,man 手册里面的内容都是英文的,这就要求你有一定的英文基础。man 手册的内容很多,涉及了 Linux 使用过程中的方方面面。为了便于查找,man 手册被进行了分册(分区段)处理,在 Research UNIX、BSD、OS X 和 Linux 中,手册通常被分为 8 个区段,安排如下:

区段 说明
1 一般命令
2 系统调用
3 库函数,涵盖了 C 标准函数库
4 特殊文件(通常是/dev 中的设备)和驱动程序
5 文件格式和约定
6 游戏和屏保
7 杂项
8 系统管理命令和守护进程

要查看相应区段的内容,就在 man 后面加上相应区段的数字即可,如:

1
man 1 ls

会显示第一区段中的ls命令 man 页面。

所有的手册页遵循一个常见的布局,为了通过简单的 ASCII 文本展示而被优化,而这种情况下可能没有任何形式的高亮或字体控制。一般包括以下部分内容:

NAME(名称)

该命令或函数的名称,接着是一行简介。

SYNOPSIS(概要)

对于命令,正式的描述它如何运行,以及需要什么样的命令行参数。对于函数,介绍函数所需的参数,以及哪个头文件包含该函数的定义。

DESCRIPTION(说明)

命令或函数功能的文本描述。

EXAMPLES(示例)

常用的一些示例。

SEE ALSO(参见)

相关命令或函数的列表。

也可能存在其它部分内容,但这些部分没有得到跨手册页的标准化。常见的例子包括:OPTIONS(选项),EXIT STATUS(退出状态),ENVIRONMENT(环境),BUGS(程序漏洞),FILES(文件),AUTHOR(作者),REPORTING BUGS(已知漏洞),HISTORY(历史)和 COPYRIGHT(版权)。

通常 man 手册中的内容很多,你可能不太容易找到你想要的结果,不过幸运的是你可以在 man 中使用搜索/<你要搜索的关键字>,查找完毕后你可以使用n键切换到下一个关键字所在处,shift+n为上一个关键字所在处。使用Space(空格键)翻页,Enter(回车键)向下滚动一行,或者使用kj(vim 编辑器的移动键)进行向前向后滚动一行。按下h键为显示使用帮助(因为 man 使用 less 作为阅读器,实为less工具的帮助),按下q退出。

想要获得更详细的帮助,你还可以使用info命令,不过通常使用man就足够了。如果你知道某个命令的作用,只是想快速查看一些它的某个具体参数的作用,那么你可以使用--help参数,大部分命令都会带有这个参数,如:

1
ls --help

用户及文件权限管理

创建用户及切换用户

su <user> 可以切换到用户 user

sudo <cmd> 可以以特权级别运行 cmd 命令

su - <user> 命令也是切换用户,但是同时用户的环境变量和工作目录也会跟着改变成目标用户所对应的。

1
sudo adduser lilei

使用如下命令切换登录用户:

1
su -l lilei

用户组

在 Linux 里面如何知道自己属于哪些用户组

方法一:使用 groups 命令

1
groups shiyanlou

此处输入图片的描述

方法二:查看 /etc/group 文件

1
cat /etc/group | sort

这里 cat 命令用于读取指定文件的内容并打印到终端输出,后面会详细讲它的使用。 | sort 表示将读取的文本进行一个字典排序再输出,然后你将看到如下一堆输出,你可以在最下面看到 shiyanlou 的用户组信息:

可以使用 grep 命令过滤掉一些你不想看到的结果:

1
cat /etc/group | grep -E "shiyanlou"

此处输入图片的描述

/etc/group 文件格式说明

/etc/group 的内容包括用户组(Group)、用户组口令、GID(组 ID) 及该用户组所包含的用户(User),每个用户组一条记录。格式如下:

group_name:password:GID:user_list

你看到上面的 password 字段为一个 x,并不是说密码就是它,只是表示密码不可见而已。

这里需要注意,如果用户的 GID 等于用户组的 GID,那么最后一个字段 user_list 就是空的

使用 usermod 命令可以为用户添加用户组,同样使用该命令你必需有 root 权限,你可以直接使用 root 用户为其它用户添加用户组,或者用其它已经在 sudo 用户组的用户使用 sudo 命令获取权限来执行该命令。

这里我用 shiyanlou 用户执行 sudo 命令将 lilei 添加到 sudo 用户组,让它也可以使用 sudo 命令获得 root 权限,首先我们切换回 shiyanlou 用户。

1
su - shiyanlou

此处需要输入 shiyanlou 用户密码,shiyanlou 的密码可以在右侧工具栏的环境信息里看到。

当然也可以通过 sudo passwd shiyanlou 进行设置,或者你直接关闭当前终端打开一个新的终端。

1
2
3
groups lilei
sudo usermod -G sudo lilei
groups lilei

image-20220824143829708

删除用户是很简单的事:

1
sudo deluser lilei --remove-home

删除用户组可以使用 groupdel 命令,倘若该群组中仍包括某些用户,则必须先删除这些用户后,才能删除群组。

查看文件权限

pic

pic

修改文件拥有者

image-20220824151419258

修改文件权限

image-20220824151812591

目录结构及文件基本操作

Linux目录结构

FHS(英文:Filesystem Hierarchy Standard 中文:文件系统层次结构标准)

img

此处输入图片的描述

使用 pwd 获取当前路径:

1
pwd

新建目录

使用 mkdir(make directories)命令可以创建一个空目录,也可同时指定创建目录的权限属性。

创建名为“ mydir ”的空目录:

1
mkdir mydir

使用 -p 参数,同时创建父目录(如果不存在该父目录),如下我们同时创建一个多级目录(这在安装软件、配置安装路径时非常有用):

1
mkdir -p father/son/grandson

复制文件

使用 cp 命令(copy)复制一个文件到指定目录。

将之前创建的 test 文件复制到 /home/shiyanlou/father/son/grandson 目录中:

1
cp test father/son/grandson

是不是很方便啊,如果在图形界面则需要先在源目录复制文件,再进到目的目录粘贴文件,而命令行操作步骤就一步到位了嘛。

复制目录

如果直接使用 cp 命令复制一个目录的话,会出现错误

要成功复制目录需要加上 -r 或者 -R 参数,表示递归复制,就是说有点“株连九族”的意思:

1
2
3
cd /home/shiyanlou
mkdir family
cp -r father family

删除文件

使用 rm(remove files or directories)命令删除一个文件:

1
rm test

有时候你会遇到想要删除一些为只读权限的文件,直接使用 rm 删除会显示一个提示,如下:

4-3.3-1

你如果想忽略这提示,直接删除文件,可以使用 -f 参数强制删除:

1
rm -f test

删除目录

跟复制目录一样,要删除一个目录,也需要加上 -r-R 参数:

1
rm -r family

遇到权限不足删除不了的目录也可以和删除文件一样加上 -f 参数:

1
rm -rf family

移动文件

例如将文件“ file1 ”移动到 Documents 目录:

1
2
3
mkdir Documents
touch file1
mv file1 Documents

重命名文件

mv 命令除了能移动文件外,还能给文件重命名。命令格式为 mv 旧的文件名 新的文件名

例如将文件“ file1 ”重命名为“ myfile ”:

1
mv file1 myfile

批量重命名

rename 命令并不是内置命令,若提示无该命令可以使用 sudo apt-get install rename 命令自行安装。

1
2
3
4
5
6
7
8
9
10
cd /home/shiyanlou/

# 使用通配符批量创建 5 个文件:
touch file{1..5}.txt

# 批量将这 5 个后缀为 .txt 的文本文件重命名为以 .c 为后缀的文件:
rename 's/\.txt/\.c/' *.txt

# 批量将这 5 个文件,文件名和后缀改为大写:
rename 'y/a-z/A-Z/' *.c

简单解释一下上面的命令,rename 是先使用第二个参数的通配符匹配所有后缀为 .txt 的文件,然后使用第一个参数提供的正则表达式将匹配的这些文件的 .txt 后缀替换为 .c,这一点在我们后面学习了 sed 命令后,相信你会更好地理解。

查看文件

cat可以加上 -n 参数显示行号:

1
cat -n passwd

nl 命令,添加行号并打印,这是个比 cat -n 更专业的行号打印命令。

这里简单列举它的常用的几个参数:

1
2
3
4
5
6
7
8
-b : 指定添加行号的方式,主要有两种:
-b a:表示无论是否为空行,同样列出行号("cat -n"就是这种方式)
-b t:只列出非空行的编号并列出(默认为这种方式)
-n : 设置行号的样式,主要有三种:
-n ln:在行号字段最左端显示
-n rn:在行号字段最右边显示,且不加 0
-n rz:在行号字段最右边显示,且加 0
-w : 行号字段占用的位数(默认为 6 位)

使用 moreless 命令分页查看文件

使用 more 命令打开 passwd 文件:

1
more passwd

可以使用 Enter 键向下滚动一行,使用 Space 键向下滚动一屏,按下 h 显示帮助,q 退出。

headtail

关于 tail 命令,不得不提的还有它一个很牛的参数 -f,这个参数可以实现不停地读取某个文件的内容并显示。这可以让我们动态查看日志,达到实时监视的目的。

查看文件类型

image-20220824160000140

环境变量与文件查找

使用 declare 命令创建一个变量名为 tmp 的变量:

1
declare tmp

使用 = 号赋值运算符,将变量 tmp 赋值为 shiyanlou。注意,与其他语言不同的是, Shell 中的赋值操作,= 两边不可以输入空格,否则会报错。

1
2
3
4
5
# 正确的赋值
tmp=shiyanlou

# 错误的赋值
tmp = shiyanlou

读取变量的值,使用 echo 命令和 $ 符号(**$ 符号用于表示引用一个变量的值,初学者经常忘记输入**):

echo $tmp

变量类型

通常我们会涉及到的变量类型有三种:

  • 当前 Shell 进程私有用户自定义变量,如上面我们创建的 tmp 变量,只在当前 Shell 中有效。
  • Shell 本身内建的变量。
  • 从自定义变量导出的环境变量。

也有三个与上述三种环境变量相关的命令:setenvexport。这三个命令很相似,都是用于打印环境变量信息,区别在于涉及的变量范围不同。详见下表:

命 令 说 明
set 显示当前 Shell 所有变量,包括其内建环境变量(与 Shell 外观等相关),用户自定义变量及导出的环境变量。
env 显示与当前用户相关的环境变量,还可以让命令在指定环境中运行。
export 显示从 Shell 中导出成环境变量的变量,也能通过它将自定义变量导出为环境变量。

1

你可以更直观的使用 vimdiff 工具比较一下它们之间的差别:

1
2
3
4
5
temp=shiyanlou
export temp_env=shiyanlou
env|sort>env.txt
export|sort>export.txt
set|sort>set.txt

上述操作将命令输出通过管道 | 使用 sort 命令排序,再重定向到对象文本文件中。管道的概念后面我们会学到,现在你知道这是什么意思就行了。

1
vimdiff env.txt export.txt set.txt

环境变量如何永久生效

当你关机后,或者关闭当前的 shell 之后,环境变量就没了啊。怎么才能让环境变量永久生效呢?

按变量的生存周期来划分,Linux 变量可分为两类:

  1. 永久的:需要修改配置文件,变量永久生效;
  2. 临时的:使用 export 命令行声明即可,变量在关闭 shell 时失效。

这里介绍两个重要文件 /etc/bashrc(有的 Linux 没有这个文件) 和 /etc/profile ,它们分别存放的是 shell 变量和环境变量。还有要注意区别的是每个用户目录下的一个隐藏文件:

1
2
3
# .profile 可以用 ls -a 查看
cd /home/shiyanlou
ls -a

.profile

这个 .profile 只对当前用户永久生效。因为它保存在当前用户的 Home 目录下,当切换用户时,工作目录可能一并被切换到对应的目录中,这个文件就无法生效。而写在 /etc/profile 里面的是对所有用户永久生效,所以如果想要添加一个永久生效的环境变量,只需要打开 /etc/profile,在最后加上你想添加的环境变量就好啦。

命令查找

查看 PATH 环境变量的内容:

1
echo $PATH

创建一个 Shell 脚本文件,你可以使用 gedit,vim,sublime 等工具编辑。如果你是直接复制的话,建议使用 gedit 或者 sublime,否则可能导致代码缩进混乱。

1
2
3
cd /home/shiyanlou
touch hello_shell.sh
gedit hello_shell.sh

在脚本中添加如下内容,保存并退出。

注意不要省掉第一行,这不是注释,有用户反映有语法错误,就是因为没有了第一行。

1
2
3
4
5
6
7
#!/bin/bash

for ((i=0; i<10; i++));do
echo "hello shell"
done

exit 0

为文件添加可执行权限,否则执行会报错没有权限:

1
chmod 755 hello_shell.sh

执行脚本:

1
2
cd /home/shiyanlou
./hello_shell.sh

创建一个 C 语言 hello world 程序:

1
2
cd /home/shiyanlou
gedit hello_world.c

输入如下内容,同样不能省略第一行。

1
2
3
4
5
6
7
#include <stdio.h>

int main(void)
{
printf("hello world!\n");
return 0;
}

保存后使用 gcc 生成可执行文件:

1
gcc -o hello_world hello_world.c

gcc 生成二进制文件默认具有可执行权限,不需要修改。

/home/shiyanlou 家目录创建一个 mybin 目录,并将上述 hello_shell.shhello_world 文件移动到其中:

1
2
3
cd /home/shiyanlou
mkdir mybin
mv hello_shell.sh hello_world mybin/

现在你可以在 mybin 目录中分别运行你刚刚创建的两个程序:

1
2
3
cd mybin
./hello_shell.sh
./hello_world

添加路径至环境变量中

在前面我们应该注意到 PATH 里面的路径是以 : 作为分割符的,所以我们可以这样添加自定义路径:

1
PATH=$PATH:/home/shiyanlou/mybin

注意这里一定要使用绝对路径。

现在你就可以在任意目录执行那两个命令了(注意需要去掉前面的 ./)。你可能会意识到这样还并没有很好的解决问题,因为我给 PATH 环境变量追加了一个路径,它也只是在当前 Shell 有效,我一旦退出终端,再打开就会发现又失效了。有没有方法让添加的环境变量全局有效?或者每次启动 Shell 时自动执行上面添加自定义路径到 PATH 的命令?下面我们就来说说后一种方式——让它自动执行。

在每个用户的 home 目录中有一个 Shell 每次启动时会默认执行一个配置脚本,以初始化环境,包括添加一些用户自定义环境变量等等。实验楼的环境使用的 Shell 是 zsh,它的配置文件是 .zshrc,相应的如果使用的 Shell 是 Bash,则配置文件为 .bashrc。它们在 etc 下还都有一个或多个全局的配置文件,不过我们一般只修改用户目录下的配置文件。Shell 的种类有很多,可以使用 cat /etc/shells 命令查看当前系统已安装的 Shell。

我们可以简单地使用下面命令直接添加内容到 .zshrc 中:

1
echo "PATH=$PATH:/home/shiyanlou/mybin" >> .zshrc

上述命令中 >> 表示将标准输出以追加的方式重定向到一个文件中,注意前面用到的 > 是以覆盖的方式重定向到一个文件中,使用的时候一定要注意分辨。在指定文件不存在的情况下都会创建新的文件。

变量修改

变量的修改有以下几种方式:

变量设置方式 说明
${变量名#匹配字串} 从头向后开始匹配,删除符合匹配字串的最短数据
${变量名##匹配字串} 从头向后开始匹配,删除符合匹配字串的最长数据
${变量名%匹配字串} 从尾向前开始匹配,删除符合匹配字串的最短数据
${变量名%%匹配字串} 从尾向前开始匹配,删除符合匹配字串的最长数据
${变量名/旧的字串/新的字串} 将符合旧字串的第一个字串替换为新的字串
${变量名//旧的字串/新的字串} 将符合旧字串的全部字串替换为新的字串

比如我们可以修改前面添加到 PATH 的环境变量,将添加的 mybin 目录从环境变量里删除。为了避免操作失误导致命令找不到,我们先将 PATH 赋值给一个新的自定义变量 mypath:

1
2
3
4
5
mypath=$PATH
echo $mypath
mypath=${mypath%/home/shiyanlou/mybin}
# 或使用通配符 * 表示任意多个任意字符
mypath=${mypath%*/mybin}

变量删除

可以使用 unset 命令删除一个环境变量:

1
unset mypath

变量立即生效

前面我们在 Shell 中修改了一个配置脚本文件之后(比如 zsh 的配置文件 home 目录下的 .zshrc),每次都要退出终端重新打开甚至重启主机之后其才能生效,很是麻烦,我们可以使用 source 命令来让其立即生效,如:

1
2
cd /home/shiyanlou
source .zshrc

source 命令还有一个别名就是 .,上面的命令如果替换成 . 的方式就该是:

1
. ./.zshrc

搜索文件

与搜索相关的命令常用的有 whereiswhichfindlocate

  • whereis 简单快速
1
2
whereis who
whereis find

你会看到 whereis find 找到了三个路径,两个可执行文件路径和一个 man 在线帮助文件所在路径,这个搜索很快,因为它并没有从硬盘中依次查找,而是直接从数据库中查询。

whereis 只能搜索二进制文件(-b),man 帮助文件(-m)和源代码文件(-s)。如果想要获得更全面的搜索结果可以使用 locate 命令。

  • locate 快而全

使用 locate 命令查找文件也不会遍历硬盘,它通过查询 /var/lib/mlocate/mlocate.db 数据库来检索信息。不过这个数据库也不是实时更新的,系统会使用定时任务每天自动执行 updatedb 命令来更新数据库。所以有时候你刚添加的文件,它可能会找不到,需要手动执行一次 updatedb 命令(在我们的环境中必须先执行一次该命令)。注意这个命令也不是内置的命令,在部分环境中需要手动安装,然后执行更新。

1
2
3
sudo apt-get update
sudo apt-get install locate
sudo updatedb

它可以用来查找指定目录下的不同文件类型,如查找 /etc 下所有以 sh 开头的文件:

1
locate /etc/sh

注意,它不只是在 /etc 目录下查找,还会自动递归子目录进行查找。

查找 /usr/share/ 下所有 jpg 文件:

1
locate /usr/share/*.jpg

环境里使用 zsh,在 ~/.zshrc 文件里添加了 setopt nonomatch 配置,这样就不会自动处理和修复命令,因此可以不使用 \ 转义。如果其他环境中执行该命令提示 zsh: no matches found: /usr/share/*.jpg,则可以在 .zshrc 中添加上述配置,或者使用 \ 转义。

如果想只统计数目可以加上 -c 参数,-i 参数可以忽略大小写进行查找,whereis-b-m-s 同样可以使用。

  • which 小而精

which 本身是 Shell 内建的一个命令,我们通常使用 which 来确定是否安装了某个指定的程序,因为它只从 PATH 环境变量指定的路径中去搜索命令并且返回第一个搜索到的结果。也就是说,我们可以看到某个系统命令是否存在以及执行的到底是哪一个地方的命令。

1
2
3
which man
which nginx
which ping
  • find 精而细

find 应该是这几个命令中最强大的了,它不但可以通过文件类型、文件名进行查找而且可以根据文件的属性(如文件的时间戳,文件的权限等)进行搜索。find 命令强大到,要把它讲明白至少需要单独好几节课程才行,我们这里只介绍一些常用的内容。

这条命令表示去 /etc/ 目录下面 ,搜索名字叫做 interfaces 的文件或者目录。这是 find 命令最常见的格式,千万记住 find 的第一个参数是要搜索的地方。命令前面加上 sudo 是因为 shiyanlou 只是普通用户,对 /etc 目录下的很多文件都没有访问的权限,如果是 root 用户则不用使用。

1
sudo find /etc/ -name interfaces

注意 find 命令的路径是作为第一个参数的, 基本命令格式为 find [path][option] [action] 。

与时间相关的命令参数:

参数 说明
-atime 最后访问时间
-ctime 最后修改文件内容的时间
-mtime 最后修改文件属性的时间

下面以 -mtime 参数举例:

  • -mtime n:n 为数字,表示为在 n 天之前的“一天之内”修改过的文件
  • -mtime +n:列出在 n 天之前(不包含 n 天本身)被修改过的文件
  • -mtime -n:列出在 n 天之内(包含 n 天本身)被修改过的文件
  • -newer file:file 为一个已存在的文件,列出比 file 还要新的文件名

列出 home 目录中,当天(24 小时之内)有改动的文件:

1
find ~ -mtime 0

列出用户家目录下比 /etc 目录新的文件:

1
find ~ -newer /etc

挑战:寻找文件

image-20220824171942624