code-rule-utils.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. const TYPE_DEFAULTS = {
  2. DATE: {
  3. segmentValue: 'yyyyMMdd',
  4. segmentLength: 8,
  5. },
  6. FIXED: {
  7. segmentValue: 'CODE',
  8. segmentLength: 4,
  9. },
  10. SEQ: {
  11. segmentValue: null,
  12. segmentLength: 4,
  13. padEnabled: 1,
  14. padChar: '0',
  15. padDirection: 'LEFT',
  16. radixType: 'DECIMAL',
  17. resetEnabled: 1,
  18. resetPolicy: 'DAY',
  19. startValue: 1,
  20. },
  21. VARIABLE: {
  22. segmentValue: null,
  23. variableSource: 'CUSTOM',
  24. segmentLength: 8,
  25. },
  26. SYS_VAR: {
  27. segmentValue: 'tenantId',
  28. segmentLength: 8,
  29. },
  30. }
  31. /**
  32. * 分段类型展示元数据:中文标签 + 预览着色标识
  33. * 着色标识对应编辑器中的 CSS 类名后缀
  34. */
  35. export const CODE_RULE_SEGMENT_TYPE_META = {
  36. DATE: { label: '日期', tone: 'date' },
  37. FIXED: { label: '固定值', tone: 'fixed' },
  38. SEQ: { label: '流水号', tone: 'sequence' },
  39. VARIABLE: { label: '变量', tone: 'variable' },
  40. SYS_VAR: { label: '系统变量', tone: 'sysvar' },
  41. }
  42. export const CODE_RULE_AMBIGUOUS_CHARACTER_OPTIONS = [
  43. { label: 'I', value: 'I', description: '易与数字 1 混淆' },
  44. { label: 'O', value: 'O', description: '易与数字 0 混淆' },
  45. { label: 'Z', value: 'Z', description: '易与数字 2 混淆' },
  46. ]
  47. const ALL_AMBIGUOUS_CHARACTERS = CODE_RULE_AMBIGUOUS_CHARACTER_OPTIONS
  48. .map(option => option.value)
  49. .join(',')
  50. export function codeRuleSegmentTypeMeta(type) {
  51. return CODE_RULE_SEGMENT_TYPE_META[type] || { label: type || '未知', tone: 'unknown' }
  52. }
  53. /**
  54. * 规范化具体排除字符。新字段有值时以新字段为准;字段为空时才读取旧总开关。
  55. */
  56. export function normalizeExcludedCharacters(value, legacyExcludeAmbiguous = 0) {
  57. const rawValues = Array.isArray(value)
  58. ? value
  59. : String(value || '').split(/[\s,]+/)
  60. const hasExplicitValue = Array.isArray(value)
  61. ? value.length > 0
  62. : Boolean(String(value || '').trim())
  63. const selected = new Set(rawValues.map(item => String(item || '').trim().toUpperCase()))
  64. const normalized = CODE_RULE_AMBIGUOUS_CHARACTER_OPTIONS
  65. .map(option => option.value)
  66. .filter(character => selected.has(character))
  67. .join(',')
  68. if (!hasExplicitValue && Number(legacyExcludeAmbiguous) === 1)
  69. return ALL_AMBIGUOUS_CHARACTERS
  70. return normalized
  71. }
  72. let segmentSeed = 0
  73. export function createLatestRequestGuard() {
  74. let version = 0
  75. return {
  76. begin() {
  77. version += 1
  78. return version
  79. },
  80. invalidate() {
  81. version += 1
  82. },
  83. isLatest(requestVersion) {
  84. return requestVersion === version
  85. },
  86. }
  87. }
  88. export function isCanceledRequest(error) {
  89. return error?.code === 'ERR_CANCELED'
  90. || error?.name === 'CanceledError'
  91. || error?.error?.code === 'ERR_CANCELED'
  92. || error?.error?.name === 'CanceledError'
  93. }
  94. export function hasCodeRulePermission(userStore, permission) {
  95. if (userStore?.isAdmin)
  96. return true
  97. const grants = [
  98. ...(Array.isArray(userStore?.permissions) ? userStore.permissions : []),
  99. ...(Array.isArray(userStore?.apiPermissions) ? userStore.apiPermissions : []),
  100. ]
  101. return grants.includes(permission) || grants.includes('**') || grants.includes('*:*:*')
  102. }
  103. function nextSegmentKey(type) {
  104. segmentSeed += 1
  105. return `${String(type || 'segment').toLowerCase()}_${Date.now().toString(36)}_${segmentSeed.toString(36)}`
  106. }
  107. export function createCodeRuleSegment(type = 'FIXED', order = 1) {
  108. const normalizedType = TYPE_DEFAULTS[type] ? type : 'FIXED'
  109. return {
  110. segmentKey: nextSegmentKey(normalizedType),
  111. segmentOrder: order,
  112. segmentType: normalizedType,
  113. segmentValue: null,
  114. variableSource: 'CUSTOM',
  115. segmentLength: null,
  116. padEnabled: 0,
  117. padChar: null,
  118. padDirection: 'LEFT',
  119. groupEnabled: 0,
  120. includeInCode: 1,
  121. radixType: null,
  122. resetEnabled: 0,
  123. resetPolicy: 'NONE',
  124. startValue: 1,
  125. excludeAmbiguous: 0,
  126. excludedCharacters: '',
  127. ...TYPE_DEFAULTS[normalizedType],
  128. }
  129. }
  130. export function changeCodeRuleSegmentType(segment, type) {
  131. const next = createCodeRuleSegment(type, segment?.segmentOrder || 1)
  132. next.segmentKey = segment?.segmentKey || next.segmentKey
  133. next.includeInCode = Number(segment?.includeInCode) === 0 ? 0 : 1
  134. next.groupEnabled = type === 'SEQ' ? 0 : Number(segment?.groupEnabled) === 1 ? 1 : 0
  135. return next
  136. }
  137. export function changeCodeRuleVariableSource(segment, variableSource) {
  138. return {
  139. ...segment,
  140. variableSource: variableSource === 'LOWCODE' ? 'LOWCODE' : 'CUSTOM',
  141. segmentValue: null,
  142. }
  143. }
  144. export function applyLowCodeVariableMapping(
  145. segments,
  146. targetSegmentKey,
  147. mapping,
  148. currentSourceObjectId,
  149. ) {
  150. const sourceObjectId = mapping?.sourceObjectId === null || mapping?.sourceObjectId === undefined
  151. ? null
  152. : String(mapping.sourceObjectId)
  153. const fieldCode = String(mapping?.fieldCode || '').trim()
  154. if (!sourceObjectId || !fieldCode)
  155. throw new Error('低代码字段映射必须选择业务对象和字段')
  156. const targetExists = (segments || []).some(segment => segment?.segmentKey === targetSegmentKey)
  157. if (!targetExists)
  158. throw new Error('待映射的编码分段已不存在')
  159. const objectChanged = String(currentSourceObjectId || '') !== sourceObjectId
  160. const clearedSegmentKeys = []
  161. const nextSegments = (segments || []).map((segment) => {
  162. if (segment?.segmentKey === targetSegmentKey) {
  163. return {
  164. ...segment,
  165. variableSource: 'LOWCODE',
  166. segmentValue: fieldCode,
  167. }
  168. }
  169. if (objectChanged
  170. && segment?.segmentType === 'VARIABLE'
  171. && segment?.variableSource === 'LOWCODE'
  172. && segment?.segmentValue) {
  173. clearedSegmentKeys.push(segment.segmentKey)
  174. return { ...segment, segmentValue: null }
  175. }
  176. return segment
  177. })
  178. return {
  179. sourceObjectId,
  180. objectChanged,
  181. clearedSegmentKeys,
  182. segments: normalizeCodeRuleSegments(nextSegments),
  183. }
  184. }
  185. export function normalizeCodeRuleSegments(segments = []) {
  186. return [...segments]
  187. .sort((left, right) => Number(left?.segmentOrder || 0) - Number(right?.segmentOrder || 0))
  188. .map((segment, index) => {
  189. const excludedCharacters = normalizeExcludedCharacters(
  190. segment?.excludedCharacters,
  191. segment?.excludeAmbiguous,
  192. )
  193. return {
  194. ...segment,
  195. segmentKey: segment?.segmentKey || nextSegmentKey(segment?.segmentType),
  196. segmentOrder: index + 1,
  197. includeInCode: Number(segment?.includeInCode) === 0 ? 0 : 1,
  198. groupEnabled: Number(segment?.groupEnabled) === 1 ? 1 : 0,
  199. padEnabled: Number(segment?.padEnabled) === 1 ? 1 : 0,
  200. resetEnabled: Number(segment?.resetEnabled) === 1 ? 1 : 0,
  201. excludedCharacters,
  202. // 旧字段只在全选时保持 1,旧客户端仍能理解历史“全部排除”语义
  203. excludeAmbiguous: excludedCharacters === ALL_AMBIGUOUS_CHARACTERS ? 1 : 0,
  204. variableSource: segment?.segmentType === 'VARIABLE' && segment?.variableSource === 'LOWCODE'
  205. ? 'LOWCODE'
  206. : 'CUSTOM',
  207. }
  208. })
  209. }
  210. export function segmentDeclaredLength(segment) {
  211. const configured = Number(segment?.segmentLength)
  212. if (Number.isFinite(configured) && configured > 0)
  213. return configured
  214. if (['DATE', 'FIXED'].includes(segment?.segmentType))
  215. return String(segment?.segmentValue || '').length
  216. return 0
  217. }
  218. export function validateCodeRuleDraft(draft = {}, options = {}) {
  219. const { forPreview = false } = options
  220. const errors = []
  221. const warnings = []
  222. const segments = normalizeCodeRuleSegments(draft.segments)
  223. if (!forPreview) {
  224. // 基础信息只在保存时校验;预览只需要分段有效,规则编码/名称等由预览载荷兜底
  225. if (!String(draft.ruleCode || '').match(/^[A-Z]\w{0,63}$/i))
  226. errors.push('规则编码必须以字母开头,且只能包含字母、数字和下划线')
  227. if (!String(draft.ruleName || '').trim())
  228. errors.push('规则名称不能为空')
  229. if (!String(draft.category || '').trim())
  230. errors.push('编码分类不能为空')
  231. if (!String(draft.scene || '').trim())
  232. errors.push('适用场景不能为空')
  233. }
  234. if (!segments.length)
  235. errors.push('至少添加一个编码分段')
  236. if (!segments.some(segment => Number(segment.includeInCode) === 1))
  237. errors.push('至少需要一个列入编码的分段')
  238. if (segments.filter(segment => segment.segmentType === 'SEQ').length > 1)
  239. errors.push('一条规则最多只能包含一个流水号段')
  240. if (!forPreview
  241. && segments.some(segment => segment.segmentType === 'VARIABLE' && segment.variableSource === 'LOWCODE')
  242. && !draft.sourceObjectId) {
  243. errors.push('业务变量段必须选择字段来源业务对象')
  244. }
  245. const keys = new Set()
  246. segments.forEach((segment, index) => {
  247. const label = `第 ${index + 1} 段`
  248. if (keys.has(segment.segmentKey))
  249. errors.push(`${label}的稳定分段键重复`)
  250. keys.add(segment.segmentKey)
  251. if (!segment.segmentType)
  252. errors.push(`${label}未选择分段类型`)
  253. if (['DATE', 'FIXED', 'VARIABLE', 'SYS_VAR'].includes(segment.segmentType) && !String(segment.segmentValue || '').trim())
  254. errors.push(`${label}缺少配置值`)
  255. const variableName = String(segment.segmentValue || '').trim()
  256. if (segment.segmentType === 'VARIABLE'
  257. && segment.variableSource === 'CUSTOM'
  258. && variableName
  259. && !variableName.match(/^[A-Z_]\w{0,63}$/i)) {
  260. errors.push(`${label}的自定义变量名必须以字母或下划线开头,且只能包含字母、数字和下划线`)
  261. }
  262. if (segment.segmentType === 'SEQ' && !(Number(segment.segmentLength) >= 1 && Number(segment.segmentLength) <= 32))
  263. errors.push(`${label}的流水长度必须在 1 到 32 之间`)
  264. if (Number(segment.groupEnabled) === 1 && Number(segment.includeInCode) !== 1)
  265. warnings.push(`${label}参与分组但不输出,跨分组可能产生相同编号`)
  266. })
  267. const totalLength = segments
  268. .filter(segment => Number(segment.includeInCode) === 1)
  269. .reduce((total, segment) => total + segmentDeclaredLength(segment), 0)
  270. if (totalLength > 96)
  271. errors.push('编码声明总长度不能超过 96 个字符')
  272. if (!segments.some(segment => segment.segmentType === 'SEQ'))
  273. warnings.push('当前规则没有流水号段,相同输入会生成相同编码')
  274. return { valid: errors.length === 0, errors, warnings, totalLength, segments }
  275. }
  276. export function buildCodeRulePreviewPayload(draft = {}, fields = {}) {
  277. const validation = validateCodeRuleDraft(draft, { forPreview: true })
  278. return {
  279. id: draft.id || null,
  280. ruleCode: draft.ruleCode || 'preview_rule',
  281. ruleName: draft.ruleName || '编码规则预览',
  282. category: draft.category || 'COMMON',
  283. sequence: Number(draft.sampleSequence || 1),
  284. fields: { ...fields },
  285. segments: validation.segments,
  286. }
  287. }
  288. export function createEmptyCodeRuleDraft() {
  289. return {
  290. id: null,
  291. versionNo: null,
  292. ruleCode: '',
  293. ruleName: '',
  294. scene: 'COMMON',
  295. category: 'COMMON',
  296. sourceObjectId: null,
  297. sourceObjectCode: null,
  298. status: 1,
  299. inCodeList: 1,
  300. remark: '',
  301. builtin: 0,
  302. sampleSequence: 1,
  303. segments: [
  304. createCodeRuleSegment('FIXED', 1),
  305. createCodeRuleSegment('DATE', 2),
  306. createCodeRuleSegment('SEQ', 3),
  307. ],
  308. }
  309. }