lysync 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lysync/models.py ADDED
@@ -0,0 +1,383 @@
1
+ from django.apps import apps
2
+ from django.db.models import CASCADE
3
+ from util import base_model_fields as models
4
+ from util.basemodel import BaseModel
5
+
6
+
7
+ # process_list 处理顺序为 SyncModelType -> SyncModelField -> SyncExportTask
8
+ class SyncModelType(BaseModel):
9
+ name = models.CharField(max_length=50, verbose_name="模型名称", db_index=True)
10
+ app_label = models.CharField(max_length=100, verbose_name="应用标签", db_index=True)
11
+ model = models.CharField(max_length=100, verbose_name="模型名", db_index=True)
12
+ sorted = models.IntegerField(
13
+ default=0, db_index=True, is_default_query=False, is_base_query=False, verbose_name="数据处理排序"
14
+ )
15
+ process_list = models.JSONField(
16
+ verbose_name="处理函数列表", is_default_query=False, is_base_query=False, default=list
17
+ )
18
+
19
+ def get_target_model(self):
20
+ """根据 ``app_label`` / ``model`` 字符串解析出目标 Django 模型类。"""
21
+ return apps.get_model(self.app_label, self.model)
22
+
23
+ class Meta:
24
+ verbose_name = "数据模型"
25
+ ordering = ["sorted"]
26
+ order_json = ["id", "sorted"]
27
+ pageinfo = True
28
+
29
+
30
+ class SyncModelField(BaseModel):
31
+ IMPORT_TRUE = True
32
+ IMPORT_FALSE = False
33
+ IMPORT_CHOICES = ((IMPORT_TRUE, "导入"), (IMPORT_FALSE, "非导入"))
34
+ name = models.CharField(max_length=50, verbose_name="字段名", db_index=True)
35
+ field_name = models.CharField(max_length=50, verbose_name="字段名", db_index=True)
36
+ model_type = models.ForeignKey(SyncModelType, on_delete=CASCADE, verbose_name="数据模型")
37
+ is_unique = models.BooleanField(
38
+ default=False, verbose_name="是否唯一字段", help_text="可以配置多个字段,用于唯一标识数据行"
39
+ )
40
+ is_datetime_index = models.BooleanField(
41
+ default=False, verbose_name="是否日期时间索引", help_text="只能配置一个字段,用户数据同步时确定时间范围"
42
+ )
43
+ process_list = models.JSONField(
44
+ verbose_name="处理函数列表", is_default_query=False, is_base_query=False, default=list
45
+ )
46
+ is_import_column = models.BooleanField(default=True, choices=IMPORT_CHOICES, verbose_name="是否为导入列")
47
+
48
+ class Meta:
49
+ verbose_name = "数据字段"
50
+ order_json = ["create_time", "name"]
51
+ list_json = {
52
+ "model_type__name": ("数据类型", "r"),
53
+ }
54
+ pageinfo = True
55
+
56
+
57
+ class SyncDataSource(BaseModel):
58
+ DB_TYPE_MYSQL = "mysql"
59
+ DB_TYPE_TSQL = "tsql"
60
+ DB_TYPE_CHOICES = (
61
+ (DB_TYPE_MYSQL, "MySQL"),
62
+ (DB_TYPE_TSQL, "SQL Server"),
63
+ )
64
+ SOURCE_TYPE_DB = "db"
65
+ SOURCE_TYPE_CUSTOM = "custom"
66
+ SOURCE_TYPE_CHOICES = (
67
+ (SOURCE_TYPE_DB, "数据库"),
68
+ (SOURCE_TYPE_CUSTOM, "自定义"),
69
+ )
70
+ org_id = models.IntegerField(verbose_name="所属组织", is_default_query=False, is_base_query=False)
71
+
72
+ CONNECT_STATUS_CHECKING = "checking"
73
+ CONNECT_STATUS_SUCCESS = "success"
74
+ CONNECT_STATUS_FAIL = "fail"
75
+ CONNECT_STATUS_CHOICES = (
76
+ (CONNECT_STATUS_CHECKING, "检查中", "blue"),
77
+ (CONNECT_STATUS_SUCCESS, "连接成功", "green"),
78
+ (CONNECT_STATUS_FAIL, "连接失败", "red"),
79
+ )
80
+
81
+ # ========== 新增:API 认证方式枚举 ==========
82
+ AUTH_TYPE_NONE = "none"
83
+ AUTH_TYPE_LOGIN_URL = "login_url"
84
+ API_AUTH_TYPE_CHOICES = (
85
+ (AUTH_TYPE_NONE, "不认证"),
86
+ (AUTH_TYPE_LOGIN_URL, "登录URL认证"),
87
+ )
88
+ # ============================================
89
+
90
+ name = models.CharField(max_length=50, verbose_name="数据源名称")
91
+ source_type = models.CharField(max_length=50, verbose_name="数据源类型", choices=SOURCE_TYPE_CHOICES)
92
+ db_type = models.CharField(max_length=50, verbose_name="数据库类型", choices=DB_TYPE_CHOICES, null=True, blank=True)
93
+ db_host = models.CharField(max_length=50, verbose_name="数据库主机", null=True, blank=True)
94
+ db_port = models.IntegerField(verbose_name="数据库端口", null=True, blank=True)
95
+ db_user = models.CharField(max_length=50, verbose_name="数据库用户", null=True, blank=True)
96
+ db_password = models.CharField(max_length=50, verbose_name="数据库密码", null=True, blank=True)
97
+ db_name = models.CharField(max_length=50, verbose_name="数据库名称", null=True, blank=True)
98
+ db_options = models.JSONField(verbose_name="数据库选项", is_default_query=False, is_base_query=False, default=dict)
99
+ db_connect_status = models.CharField(
100
+ max_length=50,
101
+ verbose_name="数据库连接状态",
102
+ choices=CONNECT_STATUS_CHOICES,
103
+ editable=False,
104
+ null=True,
105
+ blank=True,
106
+ )
107
+ db_connect_message = models.TextField(verbose_name="数据库连接消息", null=True, blank=True, editable=False)
108
+
109
+ custom_func = models.CharField(max_length=50, verbose_name="自定义采集函数名称", null=True, blank=True)
110
+ custom_func_kwargs = models.JSONField(
111
+ verbose_name="自定义采集函数参数", is_default_query=False, is_base_query=False, default=dict
112
+ )
113
+
114
+ # ========== 调用并发与频率控制(作用于相同数据源下的所有任务) ==========
115
+ # 相同数据源下,所有任务是否能够并发执行
116
+ task_concurrency_enabled = models.BooleanField(
117
+ default=False,
118
+ verbose_name="任务可并发",
119
+ help_text="相同数据源下,所有任务是否能够并发执行。"
120
+ "开启后受【最大任务并发数】限制;关闭后任务串行执行,调用间隔必须达到【任务调用最小间隔】。",
121
+ is_default_query=False,
122
+ is_base_query=False,
123
+ )
124
+ # 并发时允许同时运行的最大任务数(仅当 task_concurrency_enabled=True 时生效)
125
+ task_max_concurrency = models.IntegerField(
126
+ default=1,
127
+ verbose_name="最大任务并发数",
128
+ help_text="相同数据源下允许同时运行的最大任务数(仅在【任务可并发】开启时生效),至少为 1。",
129
+ is_default_query=False,
130
+ is_base_query=False,
131
+ )
132
+ # 不能并发时,两次调用之间必须间隔的最小秒数(仅当 task_concurrency_enabled=False 时生效)
133
+ task_call_interval = models.IntegerField(
134
+ default=0,
135
+ verbose_name="任务调用最小间隔(秒)",
136
+ help_text="相同数据源下任务不可并发时,两次调用之间必须间隔的最小秒数(仅在【任务可并发】关闭时生效)。",
137
+ is_default_query=False,
138
+ is_base_query=False,
139
+ )
140
+ # =====================================================================
141
+
142
+ class Meta:
143
+ verbose_name = "数据源"
144
+ pageinfo = True
145
+ hide_field_list = {
146
+ "org_id": ("组织ID", "r"),
147
+ }
148
+ # 敏感字段隐藏
149
+ hide_json = ["db_password", "oauth2_client_secret"]
150
+ list_json = {
151
+ "org_id": ("组织ID", "r"),
152
+ }
153
+
154
+
155
+ EXPORT_RECORD_STATUS_CHECKING = "checking"
156
+ EXPORT_RECORD_STATUS_WAIT = "wait"
157
+ EXPORT_RECORD_STATUS_RUNNING = "running"
158
+ EXPORT_RECORD_STATUS_SUCCESS = "success"
159
+ EXPORT_RECORD_STATUS_FAIL = "fail"
160
+ EXPORT_RECORD_STATUS_STOPPING = "stopping"
161
+ EXPORT_RECORD_STATUS_STOPPED = "stopped"
162
+
163
+ EXPORT_RECORD_STATUS_CHOICES = (
164
+ (EXPORT_RECORD_STATUS_CHECKING, "检查中", "blue"),
165
+ (EXPORT_RECORD_STATUS_RUNNING, "运行中", "blue"),
166
+ (EXPORT_RECORD_STATUS_SUCCESS, "成功", "green"),
167
+ (EXPORT_RECORD_STATUS_FAIL, "失败", "red"),
168
+ (EXPORT_RECORD_STATUS_STOPPING, "停止中", "yellow"),
169
+ (EXPORT_RECORD_STATUS_STOPPED, "已停止", "orange"),
170
+ (EXPORT_RECORD_STATUS_WAIT, "等待中", "yellow"),
171
+ )
172
+ EXPLAIN_STATUS_DEFAULT = "default"
173
+ EXPLAIN_STATUS_CHECKING = "checking"
174
+ EXPLAIN_STATUS_A1 = "A1"
175
+ EXPLAIN_STATUS_A2 = "A2"
176
+ EXPLAIN_STATUS_B1 = "B1"
177
+ EXPLAIN_STATUS_B2 = "B2"
178
+ EXPLAIN_STATUS_C1 = "C1"
179
+ EXPLAIN_STATUS_C2 = "C2"
180
+ EXPLAIN_STATUS_D1 = "D1"
181
+ EXPLAIN_STATUS_D2 = "D2"
182
+ EXPLAIN_STATUS_E1 = "E1"
183
+ EXPLAIN_STATUS_FAILED = "failed"
184
+ EXPLAIN_STATUS_CHOICES = (
185
+ (EXPLAIN_STATUS_DEFAULT, "未检测", "default"),
186
+ (EXPLAIN_STATUS_CHECKING, "检测中", "default"),
187
+ (EXPLAIN_STATUS_A1, "最佳(A1)", "green"),
188
+ (EXPLAIN_STATUS_A2, "优秀(A2)", "green"),
189
+ (EXPLAIN_STATUS_B1, "良好(B1)", "blue"),
190
+ (EXPLAIN_STATUS_B2, "良好(B2)", "blue"),
191
+ (EXPLAIN_STATUS_C1, "一般(C1)", "yellow"),
192
+ (EXPLAIN_STATUS_C2, "一般(C2)", "yellow"),
193
+ (EXPLAIN_STATUS_D1, "较差(D1)", "red"),
194
+ (EXPLAIN_STATUS_D2, "较差(D2)", "red"),
195
+ (EXPLAIN_STATUS_E1, "极差(E1)", "red"),
196
+ (EXPLAIN_STATUS_FAILED, "失败", "red"),
197
+ )
198
+
199
+
200
+ class SyncExportTask(BaseModel):
201
+ org_id = models.IntegerField(verbose_name="所属组织", is_default_query=False, is_base_query=False)
202
+ name = models.CharField(max_length=50, verbose_name="任务名称")
203
+ data_source = models.ForeignKey(SyncDataSource, on_delete=CASCADE, verbose_name="数据源")
204
+ model_type = models.ForeignKey(SyncModelType, on_delete=CASCADE, verbose_name="数据模型")
205
+ sql = models.TextField(verbose_name="SQL", null=True, blank=True)
206
+ explain_status = models.CharField(
207
+ max_length=50,
208
+ verbose_name="SQL性能",
209
+ editable=False,
210
+ choices=EXPLAIN_STATUS_CHOICES,
211
+ default=EXPLAIN_STATUS_DEFAULT,
212
+ )
213
+ explain_json = models.JSONField(verbose_name="SQL性能结果", editable=False, null=True, blank=True)
214
+ last_export_time = models.DateTimeField(verbose_name="最后导出时间", editable=False, null=True, blank=True)
215
+ last_export_unique_hash = models.CharField(
216
+ max_length=32, verbose_name="最后导出唯一标识", editable=False, null=True, blank=True
217
+ )
218
+ last_status = models.CharField(
219
+ max_length=50,
220
+ verbose_name="最后状态",
221
+ editable=False,
222
+ null=True,
223
+ blank=True,
224
+ choices=EXPORT_RECORD_STATUS_CHOICES,
225
+ )
226
+ last_message = models.TextField(verbose_name="最后消息", editable=False, null=True, blank=True)
227
+ # custom fields
228
+ custom_func_kwargs = models.JSONField(
229
+ verbose_name="自定义采集函数参数",
230
+ is_default_query=False,
231
+ is_base_query=False,
232
+ help_text="此参数会合并数据源的自定义采集函数参数, 此任务参数的优先级较高",
233
+ default=dict
234
+ )
235
+ process_list = models.JSONField(
236
+ verbose_name="处理函数列表", is_default_query=False, is_base_query=False, default=list
237
+ )
238
+
239
+ # ========== 调用并发与频率控制(作用于相同的任务自身) ==========
240
+ # 相同任务自身是否能够并发执行
241
+ self_concurrency_enabled = models.BooleanField(
242
+ default=False,
243
+ verbose_name="自身可并发",
244
+ help_text="相同任务是否能够并发执行。"
245
+ "开启后受【自身最大并发数】限制;关闭后同一任务串行执行,调用间隔必须达到【自身调用最小间隔】。",
246
+ is_default_query=False,
247
+ is_base_query=False,
248
+ )
249
+ # 并发时允许同时运行的最大实例数(仅当 self_concurrency_enabled=True 时生效)
250
+ self_max_concurrency = models.IntegerField(
251
+ default=1,
252
+ verbose_name="自身最大并发数",
253
+ help_text="相同任务允许同时运行的最大实例数(仅在【自身可并发】开启时生效),至少为 1。",
254
+ is_default_query=False,
255
+ is_base_query=False,
256
+ )
257
+ # 不能并发时,两次调用之间必须间隔的最小秒数(仅当 self_concurrency_enabled=False 时生效)
258
+ self_call_interval = models.IntegerField(
259
+ default=0,
260
+ verbose_name="自身调用最小间隔(秒)",
261
+ help_text="相同任务不可并发时,两次调用之间必须间隔的最小秒数(仅在【自身可并发】关闭时生效)。",
262
+ is_default_query=False,
263
+ is_base_query=False,
264
+ )
265
+ # ===============================================================
266
+ data_start_date = models.DateField(verbose_name="数据开始日期", null=True, blank=True)
267
+
268
+ class Meta:
269
+ verbose_name = "数据导出任务"
270
+ pageinfo = True
271
+ hide_field_list = {
272
+ "id": ["ID", "r,int"],
273
+ "data_source_id": ["数据源ID", "int"],
274
+ "model_type_id": ["数据类型ID", "int"],
275
+ "explain_json": ["SQL性能结果", "json"],
276
+ }
277
+ list_json = {
278
+ "data_source__name": ("数据源", "r"),
279
+ "model_type__name": ("数据类型", "r"),
280
+ }
281
+
282
+
283
+ class SyncExportTaskHistory(BaseModel):
284
+ export_task = models.ForeignKey(SyncExportTask, on_delete=CASCADE, verbose_name="任务", null=True, blank=True)
285
+ sql = models.TextField(verbose_name="SQL")
286
+
287
+ class Meta:
288
+ verbose_name = "数据导出任务历史"
289
+ ordering = ["id"]
290
+ pageinfo = True
291
+ hide_field_list = {
292
+ "id": ["ID", "r,int"],
293
+ "export_task_id": ["任务ID", "int"],
294
+ }
295
+ list_json = {
296
+ "export_task__name": ("任务", "r"),
297
+ }
298
+
299
+
300
+ class SyncDataFingerprint(BaseModel):
301
+ org_id = models.IntegerField(verbose_name="所属组织", is_default_query=False, is_base_query=False)
302
+ export_task = models.ForeignKey(SyncExportTask, on_delete=CASCADE, verbose_name="任务")
303
+ origin_recode_unique_hash = models.CharField(max_length=32, verbose_name="原始数据唯一标识", db_index=True)
304
+ fingerprint = models.CharField(max_length=32, verbose_name="数据指纹")
305
+
306
+ class Meta:
307
+ verbose_name = "数据指纹"
308
+ unique_together = ("export_task", "origin_recode_unique_hash")
309
+
310
+
311
+ class SyncExportRecord(BaseModel):
312
+ CHECK_RECORD_STATUS_COMPLETE = "complete"
313
+ CHECK_RECORD_STATUS_WAIT = "wait"
314
+ CHECK_RECORD_STATUS_CHECKING = "checking"
315
+
316
+ CHECK_RECORD_STATUS_CHOICES = (
317
+ (CHECK_RECORD_STATUS_COMPLETE, "完成", "green"),
318
+ (CHECK_RECORD_STATUS_WAIT, "等待检查", "blue"),
319
+ (CHECK_RECORD_STATUS_CHECKING, "检查中", "yellow"),
320
+ )
321
+ org_id = models.IntegerField(verbose_name="所属组织", is_default_query=False, is_base_query=False)
322
+ export_task = models.ForeignKey(SyncExportTask, on_delete=CASCADE, verbose_name="任务")
323
+ original_sql = models.TextField(verbose_name="原始 SQL")
324
+ exec_sql = models.TextField(verbose_name="执行 SQL")
325
+ export_status = models.CharField(max_length=50, verbose_name="导出状态", choices=EXPORT_RECORD_STATUS_CHOICES)
326
+ check_status = models.CharField(
327
+ max_length=50, verbose_name="检查状态", choices=CHECK_RECORD_STATUS_CHOICES, null=True, blank=True
328
+ )
329
+ message = models.TextField(verbose_name="导出消息")
330
+ # 本次同步对应的数据日期(按天同步,前闭后开区间的起始日)
331
+ data_date = models.DateField(verbose_name="数据日期", null=True, blank=True)
332
+ # 本次导出数据的唯一标识(基于 is_unique 字段计算的 md5)列表
333
+ export_unique_hash_list = models.JSONField(verbose_name="导出数据唯一标识列表", null=True, blank=True)
334
+ export_count = models.IntegerField(verbose_name="导出数据数量", default=0)
335
+ export_total_count = models.IntegerField(
336
+ verbose_name="导出数据总数", default=0, help_text="导出数据总数,包含补充数据量"
337
+ )
338
+ last_check_message = models.TextField(verbose_name="最后补录消息", editable=False, null=True, blank=True)
339
+ last_check_record_id = models.IntegerField(verbose_name="最后补录记录ID", editable=False, null=True, blank=True)
340
+ success_check_record_count = models.IntegerField(verbose_name="成功补录数量", default=0)
341
+ last_check_time = models.DateTimeField(verbose_name="最后补录时间", editable=False, null=True, blank=True)
342
+
343
+ class Meta:
344
+ verbose_name = "数据导出记录"
345
+ ordering = ["id"]
346
+ pageinfo = True
347
+ is_operation = False
348
+ hide_json = ["export_unique_hash_list"]
349
+ hide_field_list = {
350
+ "id": ["ID", "r,int"],
351
+ "export_task_id": ["任务ID", "int"],
352
+ }
353
+ list_json = {
354
+ "export_task__name": ("任务", "r"),
355
+ }
356
+ select_label_fields = "id"
357
+
358
+
359
+ class SyncExportCheckRecord(BaseModel):
360
+ org_id = models.IntegerField(verbose_name="所属组织", is_default_query=False, is_base_query=False)
361
+ export_task = models.ForeignKey(SyncExportTask, on_delete=CASCADE, verbose_name="任务")
362
+ export_record = models.ForeignKey(SyncExportRecord, on_delete=CASCADE, verbose_name="导出记录")
363
+ original_sql = models.TextField(verbose_name="原始 SQL")
364
+ exec_sql = models.TextField(verbose_name="执行 SQL")
365
+ export_status = models.CharField(max_length=50, verbose_name="导出状态", choices=EXPORT_RECORD_STATUS_CHOICES)
366
+ message = models.TextField(verbose_name="导出消息")
367
+ export_unique_hash_list = models.JSONField(verbose_name="导出数据唯一标识列表", null=True, blank=True)
368
+ export_count = models.IntegerField(verbose_name="导出数据数量", default=0)
369
+
370
+ class Meta:
371
+ verbose_name = "数据导出补充记录"
372
+ ordering = ["id"]
373
+ pageinfo = True
374
+ is_operation = False
375
+ hide_json = ["export_unique_hash_list"]
376
+ hide_field_list = {
377
+ "id": ["ID", "r,int"],
378
+ "export_task_id": ["任务ID", "int"],
379
+ "export_record_id": ["导出记录ID", "int"],
380
+ }
381
+ list_json = {
382
+ "export_task__name": ("任务", "r"),
383
+ }
@@ -0,0 +1,3 @@
1
+ # 导入子模块以注册 Django 信号(post_save 等)。
2
+ # 批量更新信号在 apps.ready() 中通过 connect_batch_update_signal() 注册。
3
+ from . import datasource # noqa: F401
@@ -0,0 +1,46 @@
1
+ """数据源(SyncDataSource)信号。
2
+
3
+ 移植自 costexcel/signals/datasource.py,适配当前模型:
4
+
5
+ * ``DataSource`` -> :class:`~lysync.models.SyncDataSource`
6
+ * 连接检查任务 ``check_datasource_connect`` 来自 ``lysync.tasks``,队列 ``lysync_export_queue``
7
+ * 批量更新信号来自宿主 ``lybase.signals.batch_update``;该模块为宿主依赖,
8
+ 缺失时静默跳过,不影响本 app 的其余功能。
9
+
10
+ 行为:
11
+ * ``post_save``:每次保存数据源都会异步触发一次连接检查
12
+ (任务内部以 ``db_connect_status == CHECKING`` 作为开关,避免信号回环)。
13
+ * 批量更新:对同一批中的**每个**数据源各触发一次检查。
14
+ 原 costexcel 按 ``(excel_type_id, org_id)`` 去重,但 ``SyncDataSource``
15
+ 已无 ``excel_type_id`` 且无复用关系,这里改为按 ``id`` 去重,
16
+ 保证每个数据源都被检查且不会在同一批内重复检查。
17
+ """
18
+
19
+ from django.db.models.signals import pre_save, post_save
20
+ from django.dispatch import receiver
21
+ from loguru import logger
22
+ from lysync.utils import get_queue_name
23
+ from util.middleware import get_request_id
24
+
25
+ from lysync.models import SyncDataSource
26
+
27
+ logger = logger.patch(lambda record: record["extra"].update(**get_request_id()))
28
+
29
+
30
+ @receiver(post_save, sender=SyncDataSource)
31
+ def datasource_post_save(sender, instance, **kwargs):
32
+ from lysync.tasks import check_datasource_connect
33
+
34
+ logger.info("run check_datasource_connect")
35
+ if instance.is_active and instance.source_type == SyncDataSource.SOURCE_TYPE_DB:
36
+ check_datasource_connect.apply_async(
37
+ kwargs={"datasource_id": instance.id},
38
+ queue=get_queue_name(),
39
+ )
40
+
41
+
42
+ @receiver(pre_save, sender=SyncDataSource)
43
+ def datasource_pre_save(sender, instance, **kwargs):
44
+ if not instance.id and instance.source_type == SyncDataSource.SOURCE_TYPE_DB:
45
+ instance.db_connect_status = SyncDataSource.CONNECT_STATUS_CHECKING
46
+ instance.db_connect_message = ""