From 854c832c0755f7cdb6cd5eeccdb8f09545c09cdb Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 18 Jun 2026 04:15:23 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E4=BA=86=20backend=20?= =?UTF-8?q?=E7=9A=84=E9=87=8D=E6=9E=84=EF=BC=8C=E5=AE=9E=E9=AA=8C=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/migrations/0001_initial.py | 435 ++++++++++++-------- backend/models.py | 243 +++++++----- backend/utils.py | 231 ++++------- backend/view.py | 12 +- gvsdsdk/fluent.py | 614 +++++++++++++++++++---------- gvsdsdk/model_base.py | 404 ++++++++++--------- gvsdsdk/models.py | 2 +- users/paihang_views.py | 61 +-- users/tixian_shenhe_services.py | 30 +- users/views.py | 28 +- 10 files changed, 1196 insertions(+), 864 deletions(-) diff --git a/backend/migrations/0001_initial.py b/backend/migrations/0001_initial.py index 646f17b..15e83fd 100644 --- a/backend/migrations/0001_initial.py +++ b/backend/migrations/0001_initial.py @@ -1,6 +1,7 @@ -# Generated by Django 4.2.27 on 2026-06-17 17:17 +# Generated by Django 4.2.27 on 2026-06-18 03:51 from django.db import migrations, models +import django.db.models.deletion class Migration(migrations.Migration): @@ -11,173 +12,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='Permission', - fields=[ - ('perm_id', models.AutoField(primary_key=True, serialize=False, verbose_name='权限ID')), - ('perm_code', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='权限编码')), - ('perm_name', models.CharField(max_length=100, verbose_name='权限名称')), - ('description', models.CharField(blank=True, default='', max_length=200, verbose_name='权限描述')), - ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ], - options={ - 'verbose_name': '权限', - 'verbose_name_plural': '权限', - 'db_table': 'permission', - 'managed': False, - }, - ), - migrations.CreateModel( - name='Role', - fields=[ - ('role_id', models.AutoField(primary_key=True, serialize=False, verbose_name='角色ID')), - ('role_code', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='角色编码')), - ('role_name', models.CharField(max_length=50, verbose_name='角色名称')), - ('description', models.CharField(blank=True, default='', max_length=200, verbose_name='角色描述')), - ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ], - options={ - 'verbose_name': '角色', - 'verbose_name_plural': '角色', - 'db_table': 'role', - 'managed': False, - }, - ), - migrations.CreateModel( - name='RolePermission', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ], - options={ - 'verbose_name': '角色权限关联', - 'verbose_name_plural': '角色权限关联', - 'db_table': 'role_permission', - 'managed': False, - }, - ), - migrations.CreateModel( - name='UserRole', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('account_id', models.CharField(db_index=True, max_length=11, verbose_name='账号ID')), - ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), - ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), - ], - options={ - 'verbose_name': '用户角色关联', - 'verbose_name_plural': '用户角色关联', - 'db_table': 'user_role', - 'managed': False, - }, - ), - migrations.CreateModel( - name='ZuzhangRiTongji', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='组长用户ID')), - ('riqi', models.DateField(db_index=True, verbose_name='日期')), - ('yaoqing_guanshi_shu', models.IntegerField(default=0, verbose_name='邀请管事总数')), - ('shouru_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='收入总额(分佣)')), - ('fenyong_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='分佣金额')), - ], - options={ - 'verbose_name': '组长每日统计', - 'db_table': 'zuzhang_ri_tongji', - 'indexes': [models.Index(fields=['riqi', '-yaoqing_guanshi_shu'], name='zuzhang_ri__riqi_87169e_idx'), models.Index(fields=['riqi', '-shouru_zonge'], name='zuzhang_ri__riqi_1d7e51_idx')], - 'unique_together': {('yonghuid', 'riqi')}, - }, - ), - migrations.CreateModel( - name='TixianRiTongji', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('riqi', models.DateField(db_index=True, verbose_name='日期')), - ('leixing', models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], verbose_name='用户类型')), - ('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='当天提现总额(元)')), - ], - options={ - 'verbose_name': '每日提现统计', - 'db_table': 'tixian_ri_tongji', - 'indexes': [models.Index(fields=['riqi', 'leixing'], name='tixian_ri_t_riqi_c18bb2_idx')], - 'unique_together': {('riqi', 'leixing')}, - }, - ), - migrations.CreateModel( - name='ShangjiaRiTongji', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='商家用户ID')), - ('riqi', models.DateField(db_index=True, verbose_name='日期')), - ('paifa_dingdan_shu', models.IntegerField(default=0, verbose_name='派发订单总数')), - ('paifa_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='派发订单总金额')), - ('jiesuan_dingdan_shu', models.IntegerField(default=0, verbose_name='结算订单总数')), - ('jiesuan_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='结算总金额')), - ('tuikuan_dingdan_shu', models.IntegerField(default=0, verbose_name='退款订单总数')), - ('tuikuan_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='退款总金额')), - ], - options={ - 'verbose_name': '商家每日统计', - 'db_table': 'shangjia_ri_tongji', - 'indexes': [models.Index(fields=['riqi', '-paifa_jine'], name='shangjia_ri_riqi_d00f7f_idx'), models.Index(fields=['riqi', '-jiesuan_jine'], name='shangjia_ri_riqi_25772a_idx'), models.Index(fields=['riqi', '-paifa_dingdan_shu'], name='shangjia_ri_riqi_e05c54_idx')], - 'unique_together': {('yonghuid', 'riqi')}, - }, - ), - migrations.CreateModel( - name='GuanshiXufeiRiTongji', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='管事用户ID')), - ('riqi', models.DateField(db_index=True, verbose_name='日期')), - ('xufei_zongshu', models.IntegerField(default=0, verbose_name='今日续费总数')), - ('xufei_shouyi', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='续费收益金额')), - ], - options={ - 'verbose_name': '管事续费每日统计', - 'db_table': 'guanshi_xufei_ri_tongji', - 'indexes': [models.Index(fields=['riqi', '-xufei_zongshu'], name='guanshi_xuf_riqi_7c128f_idx'), models.Index(fields=['riqi', '-xufei_shouyi'], name='guanshi_xuf_riqi_8f09b5_idx')], - 'unique_together': {('yonghuid', 'riqi')}, - }, - ), - migrations.CreateModel( - name='GuanshiRiTongji', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='管事用户ID')), - ('riqi', models.DateField(db_index=True, verbose_name='日期')), - ('yaoqing_dashou_shu', models.IntegerField(default=0, verbose_name='邀请打手总数')), - ('chongzhi_dashou_shu', models.IntegerField(default=0, verbose_name='充值打手数量')), - ('shouru_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='收入总额')), - ], - options={ - 'verbose_name': '管事每日统计', - 'db_table': 'guanshi_ri_tongji', - 'indexes': [models.Index(fields=['riqi', '-yaoqing_dashou_shu'], name='guanshi_ri__riqi_075e1a_idx'), models.Index(fields=['riqi', '-chongzhi_dashou_shu'], name='guanshi_ri__riqi_78a79d_idx'), models.Index(fields=['riqi', '-shouru_zonge'], name='guanshi_ri__riqi_334f52_idx')], - 'unique_together': {('yonghuid', 'riqi')}, - }, - ), - migrations.CreateModel( - name='DashouRiTongji', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('yonghuid', models.CharField(db_index=True, max_length=7, verbose_name='打手用户ID')), - ('riqi', models.DateField(db_index=True, verbose_name='日期')), - ('jiedan_zongliang', models.IntegerField(default=0, verbose_name='接单总量')), - ('chengjiao_zongliang', models.IntegerField(default=0, verbose_name='成交单总量')), - ('jiedan_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='接单总金额')), - ('chengjiao_zonge', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='成交总金额')), - ('tuikuan_liang', models.IntegerField(default=0, verbose_name='退款订单量')), - ('tuikuan_jine', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='退款总金额')), - ], - options={ - 'verbose_name': '打手每日统计', - 'db_table': 'dashou_ri_tongji', - 'indexes': [models.Index(fields=['riqi', '-jiedan_zongliang'], name='dashou_ri_t_riqi_95a785_idx'), models.Index(fields=['riqi', '-chengjiao_zongliang'], name='dashou_ri_t_riqi_ad9c1a_idx'), models.Index(fields=['riqi', '-jiedan_zonge'], name='dashou_ri_t_riqi_5da703_idx'), models.Index(fields=['riqi', '-chengjiao_zonge'], name='dashou_ri_t_riqi_0a3156_idx')], - 'unique_together': {('yonghuid', 'riqi')}, - }, - ), migrations.CreateModel( name='AbnormalUserLog', fields=[ @@ -200,7 +34,270 @@ class Migration(migrations.Migration): 'verbose_name': '异常用户日志', 'verbose_name_plural': '异常用户日志', 'db_table': 'abnormal_user_log', - 'indexes': [models.Index(fields=['account_id'], name='abnormal_us_account_425899_idx'), models.Index(fields=['ip_address'], name='abnormal_us_ip_addr_3d612b_idx'), models.Index(fields=['create_time'], name='abnormal_us_create__ee16da_idx')], }, ), + migrations.CreateModel( + name='LeaderDailyStats', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('LeaderID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='组长用户ID')), + ('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')), + ('InvitedManagerCount', models.IntegerField(db_column='yaoqing_guanshi_shu', default=0, verbose_name='邀请管事总数')), + ('TotalIncome', models.DecimalField(db_column='shouru_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='收入总额(分佣)')), + ('CommissionAmount', models.DecimalField(db_column='fenyong_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='分佣金额')), + ], + options={ + 'verbose_name': '组长每日统计', + 'db_table': 'zuzhang_ri_tongji', + }, + ), + migrations.CreateModel( + name='ManagerDailyStats', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ManagerID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='管事用户ID')), + ('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')), + ('InvitedPlayerCount', models.IntegerField(db_column='yaoqing_dashou_shu', default=0, verbose_name='邀请打手总数')), + ('RechargedPlayerCount', models.IntegerField(db_column='chongzhi_dashou_shu', default=0, verbose_name='充值打手数量')), + ('TotalIncome', models.DecimalField(db_column='shouru_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='收入总额')), + ], + options={ + 'verbose_name': '管事每日统计', + 'db_table': 'guanshi_ri_tongji', + }, + ), + migrations.CreateModel( + name='ManagerRenewalDailyStats', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ManagerID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='管事用户ID')), + ('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')), + ('RenewalTotal', models.IntegerField(db_column='xufei_zongshu', default=0, verbose_name='今日续费总数')), + ('RenewalRevenue', models.DecimalField(db_column='xufei_shouyi', decimal_places=2, default=0.0, max_digits=12, verbose_name='续费收益金额')), + ], + options={ + 'verbose_name': '管事续费每日统计', + 'db_table': 'guanshi_xufei_ri_tongji', + }, + ), + migrations.CreateModel( + name='MerchantDailyStats', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('MerchantID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='商家用户ID')), + ('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')), + ('AssignedOrderCount', models.IntegerField(db_column='paifa_dingdan_shu', default=0, verbose_name='派发订单总数')), + ('AssignedAmount', models.DecimalField(db_column='paifa_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='派发订单总金额')), + ('SettledOrderCount', models.IntegerField(db_column='jiesuan_dingdan_shu', default=0, verbose_name='结算订单总数')), + ('SettledAmount', models.DecimalField(db_column='jiesuan_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='结算总金额')), + ('RefundOrderCount', models.IntegerField(db_column='tuikuan_dingdan_shu', default=0, verbose_name='退款订单总数')), + ('RefundAmount', models.DecimalField(db_column='tuikuan_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='退款总金额')), + ], + options={ + 'verbose_name': '商家每日统计', + 'db_table': 'shangjia_ri_tongji', + }, + ), + migrations.CreateModel( + name='Permission', + fields=[ + ('perm_id', models.AutoField(primary_key=True, serialize=False, verbose_name='权限ID')), + ('perm_code', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='权限编码')), + ('perm_name', models.CharField(max_length=100, verbose_name='权限名称')), + ('description', models.CharField(blank=True, default='', max_length=200, verbose_name='权限描述')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '权限', + 'verbose_name_plural': '权限', + 'db_table': 'permission', + 'managed': True, + }, + ), + migrations.CreateModel( + name='PlayerDailyStats', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('PlayerID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='打手用户ID')), + ('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')), + ('AcceptedOrderTotal', models.IntegerField(db_column='jiedan_zongliang', default=0, verbose_name='接单总量')), + ('CompletedOrderTotal', models.IntegerField(db_column='chengjiao_zongliang', default=0, verbose_name='成交单总量')), + ('AcceptedAmount', models.DecimalField(db_column='jiedan_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='接单总金额')), + ('CompletedAmount', models.DecimalField(db_column='chengjiao_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='成交总金额')), + ('RefundCount', models.IntegerField(db_column='tuikuan_liang', default=0, verbose_name='退款订单量')), + ('RefundAmount', models.DecimalField(db_column='tuikuan_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='退款总金额')), + ], + options={ + 'verbose_name': '打手每日统计', + 'db_table': 'dashou_ri_tongji', + }, + ), + migrations.CreateModel( + name='Role', + fields=[ + ('role_id', models.AutoField(primary_key=True, serialize=False, verbose_name='角色ID')), + ('role_code', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='角色编码')), + ('role_name', models.CharField(max_length=50, verbose_name='角色名称')), + ('description', models.CharField(blank=True, default='', max_length=200, verbose_name='角色描述')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '角色', + 'verbose_name_plural': '角色', + 'db_table': 'role', + 'managed': True, + }, + ), + migrations.CreateModel( + name='WithdrawalDailyStats', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')), + ('WithdrawalType', models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], db_column='leixing', verbose_name='用户类型')), + ('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='当天提现总额(元)')), + ], + options={ + 'verbose_name': '每日提现统计', + 'db_table': 'tixian_ri_tongji', + 'indexes': [models.Index(fields=['Date', 'WithdrawalType'], name='tixian_ri_t_riqi_c18bb2_idx')], + 'unique_together': {('Date', 'WithdrawalType')}, + }, + ), + migrations.CreateModel( + name='UserRole', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('account_id', models.CharField(db_index=True, max_length=11, verbose_name='账号ID')), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色')), + ], + options={ + 'verbose_name': '用户角色关联', + 'verbose_name_plural': '用户角色关联', + 'db_table': 'user_role', + 'managed': True, + }, + ), + migrations.CreateModel( + name='RolePermission', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False)), + ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.permission', verbose_name='权限')), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色')), + ], + options={ + 'verbose_name': '角色权限关联', + 'verbose_name_plural': '角色权限关联', + 'db_table': 'role_permission', + 'managed': True, + }, + ), + migrations.AddIndex( + model_name='role', + index=models.Index(fields=['role_code'], name='role_role_co_b9f497_idx'), + ), + migrations.AddIndex( + model_name='playerdailystats', + index=models.Index(fields=['Date', '-AcceptedOrderTotal'], name='dashou_ri_t_riqi_95a785_idx'), + ), + migrations.AddIndex( + model_name='playerdailystats', + index=models.Index(fields=['Date', '-CompletedOrderTotal'], name='dashou_ri_t_riqi_ad9c1a_idx'), + ), + migrations.AddIndex( + model_name='playerdailystats', + index=models.Index(fields=['Date', '-AcceptedAmount'], name='dashou_ri_t_riqi_5da703_idx'), + ), + migrations.AddIndex( + model_name='playerdailystats', + index=models.Index(fields=['Date', '-CompletedAmount'], name='dashou_ri_t_riqi_0a3156_idx'), + ), + migrations.AlterUniqueTogether( + name='playerdailystats', + unique_together={('PlayerID', 'Date')}, + ), + migrations.AddIndex( + model_name='permission', + index=models.Index(fields=['perm_code'], name='permission_perm_co_56cbe3_idx'), + ), + migrations.AddIndex( + model_name='merchantdailystats', + index=models.Index(fields=['Date', '-AssignedAmount'], name='shangjia_ri_riqi_d00f7f_idx'), + ), + migrations.AddIndex( + model_name='merchantdailystats', + index=models.Index(fields=['Date', '-SettledAmount'], name='shangjia_ri_riqi_25772a_idx'), + ), + migrations.AddIndex( + model_name='merchantdailystats', + index=models.Index(fields=['Date', '-AssignedOrderCount'], name='shangjia_ri_riqi_e05c54_idx'), + ), + migrations.AlterUniqueTogether( + name='merchantdailystats', + unique_together={('MerchantID', 'Date')}, + ), + migrations.AddIndex( + model_name='managerrenewaldailystats', + index=models.Index(fields=['Date', '-RenewalTotal'], name='guanshi_xuf_riqi_7c128f_idx'), + ), + migrations.AddIndex( + model_name='managerrenewaldailystats', + index=models.Index(fields=['Date', '-RenewalRevenue'], name='guanshi_xuf_riqi_8f09b5_idx'), + ), + migrations.AlterUniqueTogether( + name='managerrenewaldailystats', + unique_together={('ManagerID', 'Date')}, + ), + migrations.AddIndex( + model_name='managerdailystats', + index=models.Index(fields=['Date', '-InvitedPlayerCount'], name='guanshi_ri__riqi_075e1a_idx'), + ), + migrations.AddIndex( + model_name='managerdailystats', + index=models.Index(fields=['Date', '-RechargedPlayerCount'], name='guanshi_ri__riqi_78a79d_idx'), + ), + migrations.AddIndex( + model_name='managerdailystats', + index=models.Index(fields=['Date', '-TotalIncome'], name='guanshi_ri__riqi_334f52_idx'), + ), + migrations.AlterUniqueTogether( + name='managerdailystats', + unique_together={('ManagerID', 'Date')}, + ), + migrations.AddIndex( + model_name='leaderdailystats', + index=models.Index(fields=['Date', '-InvitedManagerCount'], name='zuzhang_ri__riqi_87169e_idx'), + ), + migrations.AddIndex( + model_name='leaderdailystats', + index=models.Index(fields=['Date', '-TotalIncome'], name='zuzhang_ri__riqi_1d7e51_idx'), + ), + migrations.AlterUniqueTogether( + name='leaderdailystats', + unique_together={('LeaderID', 'Date')}, + ), + migrations.AddIndex( + model_name='abnormaluserlog', + index=models.Index(fields=['account_id'], name='abnormal_us_account_425899_idx'), + ), + migrations.AddIndex( + model_name='abnormaluserlog', + index=models.Index(fields=['ip_address'], name='abnormal_us_ip_addr_3d612b_idx'), + ), + migrations.AddIndex( + model_name='abnormaluserlog', + index=models.Index(fields=['create_time'], name='abnormal_us_create__ee16da_idx'), + ), + migrations.AlterUniqueTogether( + name='userrole', + unique_together={('account_id', 'role')}, + ), + migrations.AlterUniqueTogether( + name='rolepermission', + unique_together={('role', 'permission')}, + ), ] diff --git a/backend/models.py b/backend/models.py index 1040f7b..fb1b377 100644 --- a/backend/models.py +++ b/backend/models.py @@ -17,7 +17,7 @@ class Role(QModel): db_table = 'role' verbose_name = '角色' verbose_name_plural = '角色' - managed = False + managed = True indexes = [ models.Index(fields=['role_code']), ] @@ -40,7 +40,7 @@ class Permission(QModel): db_table = 'permission' verbose_name = '权限' verbose_name_plural = '权限' - managed = False + managed = True indexes = [ models.Index(fields=['perm_code']), ] @@ -61,8 +61,8 @@ class RolePermission(QModel): db_table = 'role_permission' verbose_name = '角色权限关联' verbose_name_plural = '角色权限关联' - managed = False - unique_together = [['role', 'permission']] # 防止重复授权 + managed = True + unique_together = [['role', 'permission']] def __str__(self): return f"{self.role.role_name} -> {self.permission.perm_name}" @@ -72,7 +72,7 @@ class RolePermission(QModel): class UserRole(QModel): """用户角色关联表""" id = models.AutoField(primary_key=True) - account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID') # 对应 UserMain 的 phone 字段 + account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID') role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色') create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间') @@ -81,37 +81,28 @@ class UserRole(QModel): db_table = 'user_role' verbose_name = '用户角色关联' verbose_name_plural = '用户角色关联' - managed = False - unique_together = [['account_id', 'role']] # 同一用户同一角色只能有一条 + managed = True + unique_together = [['account_id', 'role']] def __str__(self): return f"{self.account_id} -> {self.role.role_name}" +# ==================== 5. 异常用户日志 ==================== class AbnormalUserLog(QModel): """记录恶意尝试越权操作的用户信息""" - # 账号信息 account_id = models.CharField(max_length=20, db_index=True, verbose_name='账号ID(phone)') - - # 网络信息 ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP地址') real_ip = models.GenericIPAddressField(null=True, blank=True, verbose_name='真实IP(穿透代理后)') x_forwarded_for = models.CharField(max_length=255, blank=True, default='', verbose_name='X-Forwarded-For') user_agent = models.CharField(max_length=500, blank=True, default='', verbose_name='User-Agent') - - # 地理位置信息(需借助第三方API,这里只存储请求时的IP,后续可离线解析) - # 但我们可以存储更多请求细节 request_method = models.CharField(max_length=10, blank=True, default='', verbose_name='请求方法') request_path = models.CharField(max_length=500, blank=True, default='', verbose_name='请求路径') request_params = models.TextField(blank=True, default='', verbose_name='请求参数(GET/POST)') request_body = models.TextField(blank=True, default='', verbose_name='请求体(原始)') request_headers = models.TextField(blank=True, default='', verbose_name='请求头(JSON格式)') - - # 攻击特征 attempt_type = models.CharField(max_length=50, blank=True, default='', verbose_name='攻击类型') detail = models.TextField(blank=True, default='', verbose_name='详细描述') - - # 时间戳 create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: @@ -128,148 +119,206 @@ class AbnormalUserLog(QModel): return f"{self.account_id} @ {self.ip_address} - {self.create_time}" - - - -class ShangjiaRiTongji(QModel): - yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='商家用户ID') - riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 +# ==================== 6. 商家每日统计 ==================== +class MerchantDailyStats(QModel): + """商家每日统计表(原 ShangjiaRiTongji)""" + MerchantID = models.CharField( + max_length=7, db_index=True, db_column='yonghuid', verbose_name='商家用户ID') + Date = models.DateField( + db_index=True, db_column='riqi', verbose_name='日期') # 派发统计 - paifa_dingdan_shu = models.IntegerField(default=0, verbose_name='派发订单总数') - paifa_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='派发订单总金额') + AssignedOrderCount = models.IntegerField( + default=0, db_column='paifa_dingdan_shu', verbose_name='派发订单总数') + AssignedAmount = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, + db_column='paifa_jine', verbose_name='派发订单总金额') - # 结算统计(成交) - jiesuan_dingdan_shu = models.IntegerField(default=0, verbose_name='结算订单总数') - jiesuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='结算总金额') + # 结算统计 + SettledOrderCount = models.IntegerField( + default=0, db_column='jiesuan_dingdan_shu', verbose_name='结算订单总数') + SettledAmount = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, + db_column='jiesuan_jine', verbose_name='结算总金额') - # 退款统计(可选) - tuikuan_dingdan_shu = models.IntegerField(default=0, verbose_name='退款订单总数') - tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额') + # 退款统计 + RefundOrderCount = models.IntegerField( + default=0, db_column='tuikuan_dingdan_shu', verbose_name='退款订单总数') + RefundAmount = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, + db_column='tuikuan_jine', verbose_name='退款总金额') class Meta: db_table = 'shangjia_ri_tongji' verbose_name = '商家每日统计' - unique_together = [['yonghuid', 'riqi']] # 每人每天一条记录 + unique_together = [['MerchantID', 'Date']] indexes = [ - models.Index(fields=['riqi', '-paifa_jine']), - models.Index(fields=['riqi', '-jiesuan_jine']), - models.Index(fields=['riqi', '-paifa_dingdan_shu']), + models.Index(fields=['Date', '-AssignedAmount']), + models.Index(fields=['Date', '-SettledAmount']), + models.Index(fields=['Date', '-AssignedOrderCount']), ] + def __str__(self): + return f"商家{self.MerchantID} {self.Date}" - - -class DashouRiTongji(QModel): - yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='打手用户ID') - riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 +# ==================== 7. 打手每日统计 ==================== +class PlayerDailyStats(QModel): + """打手每日统计表(原 DashouRiTongji)""" + PlayerID = models.CharField( + max_length=7, db_index=True, db_column='yonghuid', verbose_name='打手用户ID') + Date = models.DateField( + db_index=True, db_column='riqi', verbose_name='日期') # 接单统计 - jiedan_zongliang = models.IntegerField(default=0, verbose_name='接单总量') - chengjiao_zongliang = models.IntegerField(default=0, verbose_name='成交单总量') + AcceptedOrderTotal = models.IntegerField( + default=0, db_column='jiedan_zongliang', verbose_name='接单总量') + CompletedOrderTotal = models.IntegerField( + default=0, db_column='chengjiao_zongliang', verbose_name='成交单总量') # 金额统计 - jiedan_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='接单总金额') - chengjiao_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='成交总金额') + AcceptedAmount = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, + db_column='jiedan_zonge', verbose_name='接单总金额') + CompletedAmount = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, + db_column='chengjiao_zonge', verbose_name='成交总金额') # 退款统计 - tuikuan_liang = models.IntegerField(default=0, verbose_name='退款订单量') - tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额') + RefundCount = models.IntegerField( + default=0, db_column='tuikuan_liang', verbose_name='退款订单量') + RefundAmount = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, + db_column='tuikuan_jine', verbose_name='退款总金额') class Meta: db_table = 'dashou_ri_tongji' verbose_name = '打手每日统计' - unique_together = [['yonghuid', 'riqi']] + unique_together = [['PlayerID', 'Date']] indexes = [ - models.Index(fields=['riqi', '-jiedan_zongliang']), - models.Index(fields=['riqi', '-chengjiao_zongliang']), - models.Index(fields=['riqi', '-jiedan_zonge']), - models.Index(fields=['riqi', '-chengjiao_zonge']), + models.Index(fields=['Date', '-AcceptedOrderTotal']), + models.Index(fields=['Date', '-CompletedOrderTotal']), + models.Index(fields=['Date', '-AcceptedAmount']), + models.Index(fields=['Date', '-CompletedAmount']), ] + def __str__(self): + return f"打手{self.PlayerID} {self.Date}" -class ZuzhangRiTongji(QModel): - yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='组长用户ID') - riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 +# ==================== 8. 组长每日统计 ==================== +class LeaderDailyStats(QModel): + """组长每日统计表(原 ZuzhangRiTongji)""" + LeaderID = models.CharField( + max_length=7, db_index=True, db_column='yonghuid', verbose_name='组长用户ID') + Date = models.DateField( + db_index=True, db_column='riqi', verbose_name='日期') - yaoqing_guanshi_shu = models.IntegerField(default=0, verbose_name='邀请管事总数') - shouru_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额(分佣)') - - # 可选明细 - fenyong_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='分佣金额') + InvitedManagerCount = models.IntegerField( + default=0, db_column='yaoqing_guanshi_shu', verbose_name='邀请管事总数') + TotalIncome = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, + db_column='shouru_zonge', verbose_name='收入总额(分佣)') + CommissionAmount = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, + db_column='fenyong_jine', verbose_name='分佣金额') class Meta: db_table = 'zuzhang_ri_tongji' verbose_name = '组长每日统计' - unique_together = [['yonghuid', 'riqi']] + unique_together = [['LeaderID', 'Date']] indexes = [ - models.Index(fields=['riqi', '-yaoqing_guanshi_shu']), - models.Index(fields=['riqi', '-shouru_zonge']), + models.Index(fields=['Date', '-InvitedManagerCount']), + models.Index(fields=['Date', '-TotalIncome']), ] + def __str__(self): + return f"组长{self.LeaderID} {self.Date}" +# ==================== 9. 管事每日统计 ==================== +class ManagerDailyStats(QModel): + """管事每日统计表(原 GuanshiRiTongji)""" + ManagerID = models.CharField( + max_length=7, db_index=True, db_column='yonghuid', verbose_name='管事用户ID') + Date = models.DateField( + db_index=True, db_column='riqi', verbose_name='日期') -class GuanshiRiTongji(QModel): - yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID') - riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 - - yaoqing_dashou_shu = models.IntegerField(default=0, verbose_name='邀请打手总数') - chongzhi_dashou_shu = models.IntegerField(default=0, verbose_name='充值打手数量') - shouru_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额') + InvitedPlayerCount = models.IntegerField( + default=0, db_column='yaoqing_dashou_shu', verbose_name='邀请打手总数') + RechargedPlayerCount = models.IntegerField( + default=0, db_column='chongzhi_dashou_shu', verbose_name='充值打手数量') + TotalIncome = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, + db_column='shouru_zonge', verbose_name='收入总额') class Meta: db_table = 'guanshi_ri_tongji' verbose_name = '管事每日统计' - unique_together = [['yonghuid', 'riqi']] + unique_together = [['ManagerID', 'Date']] indexes = [ - models.Index(fields=['riqi', '-yaoqing_dashou_shu']), - models.Index(fields=['riqi', '-chongzhi_dashou_shu']), - models.Index(fields=['riqi', '-shouru_zonge']), + models.Index(fields=['Date', '-InvitedPlayerCount']), + models.Index(fields=['Date', '-RechargedPlayerCount']), + models.Index(fields=['Date', '-TotalIncome']), ] + def __str__(self): + return f"管事{self.ManagerID} {self.Date}" +# ==================== 10. 管事续费每日统计 ==================== +class ManagerRenewalDailyStats(QModel): + """管事续费每日统计表(原 GuanshiXufeiRiTongji)""" + ManagerID = models.CharField( + max_length=7, db_index=True, db_column='yonghuid', verbose_name='管事用户ID') + Date = models.DateField( + db_index=True, db_column='riqi', verbose_name='日期') -class GuanshiXufeiRiTongji(QModel): - yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID') - riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日 - - xufei_zongshu = models.IntegerField(default=0, verbose_name='今日续费总数') # 续费订单数(或续费打手数) - xufei_shouyi = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='续费收益金额') + RenewalTotal = models.IntegerField( + default=0, db_column='xufei_zongshu', verbose_name='今日续费总数') + RenewalRevenue = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, + db_column='xufei_shouyi', verbose_name='续费收益金额') class Meta: db_table = 'guanshi_xufei_ri_tongji' verbose_name = '管事续费每日统计' - unique_together = [['yonghuid', 'riqi']] + unique_together = [['ManagerID', 'Date']] indexes = [ - models.Index(fields=['riqi', '-xufei_zongshu']), - models.Index(fields=['riqi', '-xufei_shouyi']), + models.Index(fields=['Date', '-RenewalTotal']), + models.Index(fields=['Date', '-RenewalRevenue']), ] + def __str__(self): + return f"管事续费{self.ManagerID} {self.Date}" -# models.py 中添加 - -class TixianRiTongji(QModel): - """ - 每日提现统计表(按身份统计当天提现总额) - """ - riqi = models.DateField(db_index=True, verbose_name='日期') - leixing = models.IntegerField( +# ==================== 11. 提现每日统计 ==================== +class WithdrawalDailyStats(QModel): + """每日提现统计表(原 TixianRiTongji),按身份统计当天提现总额""" + Date = models.DateField( + db_index=True, db_column='riqi', verbose_name='日期') + WithdrawalType = models.IntegerField( choices=[(1, '打手'), (2, '管事'), (3, '组长'), - (4, '考核官'), (5, '打手押金'), (6, '商家余额')], - verbose_name='用户类型' - ) - total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='当天提现总额(元)') + (4, '考核官'), (5, '打手押金'), (6, '商家余额')], + db_column='leixing', verbose_name='用户类型') + total_amount = models.DecimalField( + max_digits=12, decimal_places=2, default=0.00, verbose_name='当天提现总额(元)') class Meta: db_table = 'tixian_ri_tongji' verbose_name = '每日提现统计' - unique_together = [['riqi', 'leixing']] # 每天每个身份一条记录 - indexes = [models.Index(fields=['riqi', 'leixing'])] + unique_together = [['Date', 'WithdrawalType']] + indexes = [models.Index(fields=['Date', 'WithdrawalType'])] def __str__(self): - return f"{self.riqi} - {self.get_leixing_display()} 提现总额: {self.total_amount}" \ No newline at end of file + return f"{self.Date} - {self.get_WithdrawalType_display()} 提现总额: {self.total_amount}" + + +# ==================== 兼容别名(供逐步迁移) ==================== +ShangjiaRiTongji = MerchantDailyStats +DashouRiTongji = PlayerDailyStats +ZuzhangRiTongji = LeaderDailyStats +GuanshiRiTongji = ManagerDailyStats +GuanshiXufeiRiTongji = ManagerRenewalDailyStats +TixianRiTongji = WithdrawalDailyStats diff --git a/backend/utils.py b/backend/utils.py index 6c0293d..5ab8368 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -5,8 +5,8 @@ from functools import lru_cache from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from rest_framework.response import Response -from .models import AbnormalUserLog, UserRole, Permission, DashouRiTongji, ShangjiaRiTongji, \ - GuanshiXufeiRiTongji, GuanshiRiTongji, ZuzhangRiTongji, TixianRiTongji +from .models import AbnormalUserLog, UserRole, Permission, PlayerDailyStats, MerchantDailyStats, \ + ManagerRenewalDailyStats, ManagerDailyStats, LeaderDailyStats, WithdrawalDailyStats from django.db import transaction from django.db.models import F @@ -129,36 +129,36 @@ def update_dashou_daily_by_action(yonghuid, amount, action): today = date.today() with transaction.atomic(): - stat, created = DashouRiTongji.objects.select_for_update().get_or_create( - yonghuid=yonghuid, - riqi=today, + stat, created = PlayerDailyStats.objects.select_for_update().get_or_create( + PlayerID=yonghuid, + Date=today, defaults={ - 'yonghuid': yonghuid, - 'riqi': today, - 'jiedan_zongliang': 0, - 'jiedan_zonge': Decimal('0.00'), - 'chengjiao_zongliang': 0, - 'chengjiao_zonge': Decimal('0.00'), - 'tuikuan_liang': 0, - 'tuikuan_jine': Decimal('0.00'), + 'PlayerID': yonghuid, + 'Date': today, + 'AcceptedOrderTotal': 0, + 'AcceptedAmount': Decimal('0.00'), + 'CompletedOrderTotal': 0, + 'CompletedAmount': Decimal('0.00'), + 'RefundCount': 0, + 'RefundAmount': Decimal('0.00'), } ) update_fields = {} if action == 1: # 接单 - update_fields['jiedan_zongliang'] = F('jiedan_zongliang') + 1 - update_fields['jiedan_zonge'] = F('jiedan_zonge') + amount + update_fields['AcceptedOrderTotal'] = F('AcceptedOrderTotal') + 1 + update_fields['AcceptedAmount'] = F('AcceptedAmount') + amount elif action == 2: # 成交 - update_fields['chengjiao_zongliang'] = F('chengjiao_zongliang') + 1 - update_fields['chengjiao_zonge'] = F('chengjiao_zonge') + amount + update_fields['CompletedOrderTotal'] = F('CompletedOrderTotal') + 1 + update_fields['CompletedAmount'] = F('CompletedAmount') + amount elif action == 3: # 退款 - update_fields['tuikuan_liang'] = F('tuikuan_liang') + 1 - update_fields['tuikuan_jine'] = F('tuikuan_jine') + amount + update_fields['RefundCount'] = F('RefundCount') + 1 + update_fields['RefundAmount'] = F('RefundAmount') + amount else: return # 无效行为 if update_fields: - DashouRiTongji.query.filter(id=stat.id).update(**update_fields) + PlayerDailyStats.query.filter(id=stat.id).update(**update_fields) def update_shangjia_daily(yonghuid, amount, action): @@ -183,38 +183,38 @@ def update_shangjia_daily(yonghuid, amount, action): with transaction.atomic(): # 获取或创建当天统计记录,同时加锁防止并发 - stat, created = ShangjiaRiTongji.objects.select_for_update().get_or_create( - yonghuid=yonghuid, - riqi=today, + stat, created = MerchantDailyStats.objects.select_for_update().get_or_create( + MerchantID=yonghuid, + Date=today, defaults={ - 'yonghuid': yonghuid, - 'riqi': today, - 'paifa_dingdan_shu': 0, - 'paifa_jine': Decimal('0.00'), - 'jiesuan_dingdan_shu': 0, - 'jiesuan_jine': Decimal('0.00'), - 'tuikuan_dingdan_shu': 0, - 'tuikuan_jine': Decimal('0.00'), + 'MerchantID': yonghuid, + 'Date': today, + 'AssignedOrderCount': 0, + 'AssignedAmount': Decimal('0.00'), + 'SettledOrderCount': 0, + 'SettledAmount': Decimal('0.00'), + 'RefundOrderCount': 0, + 'RefundAmount': Decimal('0.00'), } ) # 构建本次更新的字段,使用 F 表达式进行原子累加 update_fields = {} if action == 1: # 派发 - update_fields['paifa_dingdan_shu'] = F('paifa_dingdan_shu') + 1 - update_fields['paifa_jine'] = F('paifa_jine') + amount + update_fields['AssignedOrderCount'] = F('AssignedOrderCount') + 1 + update_fields['AssignedAmount'] = F('AssignedAmount') + amount elif action == 2: # 结算(成交) - update_fields['jiesuan_dingdan_shu'] = F('jiesuan_dingdan_shu') + 1 - update_fields['jiesuan_jine'] = F('jiesuan_jine') + amount + update_fields['SettledOrderCount'] = F('SettledOrderCount') + 1 + update_fields['SettledAmount'] = F('SettledAmount') + amount elif action == 3: # 退款 - update_fields['tuikuan_dingdan_shu'] = F('tuikuan_dingdan_shu') + 1 - update_fields['tuikuan_jine'] = F('tuikuan_jine') + amount + update_fields['RefundOrderCount'] = F('RefundOrderCount') + 1 + update_fields['RefundAmount'] = F('RefundAmount') + amount else: # 无效的行为类型,直接返回 return # 执行原子更新 - ShangjiaRiTongji.query.filter(id=stat.id).update(**update_fields) + MerchantDailyStats.query.filter(id=stat.id).update(**update_fields) @@ -226,26 +226,26 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')): today = date.today() with transaction.atomic(): - stat, created = GuanshiRiTongji.objects.select_for_update().get_or_create( - yonghuid=yonghuid, - riqi=today, + stat, created = ManagerDailyStats.objects.select_for_update().get_or_create( + ManagerID=yonghuid, + Date=today, defaults={ - 'yonghuid': yonghuid, - 'riqi': today, - 'yaoqing_dashou_shu': 0, - 'chongzhi_dashou_shu': 0, - 'shouru_zonge': Decimal('0.00'), + 'ManagerID': yonghuid, + 'Date': today, + 'InvitedPlayerCount': 0, + 'RechargedPlayerCount': 0, + 'TotalIncome': Decimal('0.00'), } ) if action == 1: # 邀请打手:无论新创建还是已存在,直接 +1 if created: - stat.yaoqing_dashou_shu = 1 + stat.InvitedPlayerCount = 1 stat.save() else: - GuanshiRiTongji.query.filter(id=stat.id).update( - yaoqing_dashou_shu=F('yaoqing_dashou_shu') + 1 + ManagerDailyStats.query.filter(id=stat.id).update( + InvitedPlayerCount=F('InvitedPlayerCount') + 1 ) elif action == 2: @@ -253,13 +253,13 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')): if amount <= 0: return if created: - stat.chongzhi_dashou_shu = 1 - stat.shouru_zonge = amount + stat.RechargedPlayerCount = 1 + stat.TotalIncome = amount stat.save() else: - GuanshiRiTongji.query.filter(id=stat.id).update( - chongzhi_dashou_shu=F('chongzhi_dashou_shu') + 1, - shouru_zonge=F('shouru_zonge') + amount + ManagerDailyStats.query.filter(id=stat.id).update( + RechargedPlayerCount=F('RechargedPlayerCount') + 1, + TotalIncome=F('TotalIncome') + amount ) elif action in (3, 4): @@ -267,70 +267,13 @@ def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')): if amount <= 0: return if created: - stat.shouru_zonge = amount + stat.TotalIncome = amount stat.save() else: - GuanshiRiTongji.query.filter(id=stat.id).update( - shouru_zonge=F('shouru_zonge') + amount + ManagerDailyStats.query.filter(id=stat.id).update( + TotalIncome=F('TotalIncome') + amount ) -'''def update_guanshi_daily_by_action(yonghuid, action, amount=Decimal('0.00')): - """ - 根据操作行为更新管事每日统计(邀请打手、充值会员) - - 参数: - yonghuid: 管事用户ID - action: 操作类型 - 1 = 邀请打手(次数自动+1,无需传其他参数) - 2 = 充值会员(需要传 amount 参数) - amount: 充值金额(Decimal),仅在 action=2 时有效 - - 说明: - - 每次调用只代表一次操作,不会批量处理。 - - 使用 select_for_update + F() 保证原子性和并发安全。 - """ - today = date.today() - - # 邀请打手:次数+1,金额不变 - if action == 1: - count_field = 'yaoqing_dashou_shu' - amount = Decimal('0.00') - - # 充值会员:次数+1,金额累加 - elif action == 2: - amount = Decimal(str(amount)) if amount else Decimal('0.00') - if amount <= 0: - return - count_field = 'chongzhi_dashou_shu' - - else: - return # 无效行为,直接返回 - - with transaction.atomic(): - stat, created = GuanshiRiTongji.objects.select_for_update().get_or_create( - yonghuid=yonghuid, - riqi=today, - defaults={ - 'yonghuid': yonghuid, - 'riqi': today, - 'yaoqing_dashou_shu': 0, - 'chongzhi_dashou_shu': 0, - 'shouru_zonge': Decimal('0.00'), - } - ) - - if created: - # 新记录直接赋初值 - if action == 1: - stat.yaoqing_dashou_shu = 1 - else: - # 已存在记录,使用 F 表达式原子累加次数 - update_fields = {count_field: F(count_field) + 1} - - if action == 2: - update_fields['shouru_zonge'] = F('shouru_zonge') + amount - - GuanshiRiTongji.query.filter(id=stat.id).update(**update_fields)''' def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00')): """ @@ -339,20 +282,20 @@ def update_guanshi_xufei_daily(yonghuid, xufei_jine=Decimal('0.00')): """ today = date.today() with transaction.atomic(): - stat, created = GuanshiXufeiRiTongji.objects.select_for_update().get_or_create( - yonghuid=yonghuid, - riqi=today, + stat, created = ManagerRenewalDailyStats.objects.select_for_update().get_or_create( + ManagerID=yonghuid, + Date=today, defaults={ - 'yonghuid': yonghuid, - 'riqi': today, - 'xufei_zongshu': 1, - 'xufei_shouyi': xufei_jine, + 'ManagerID': yonghuid, + 'Date': today, + 'RenewalTotal': 1, + 'RenewalRevenue': xufei_jine, } ) if not created: - GuanshiXufeiRiTongji.query.filter(id=stat.id).update( - xufei_zongshu=F('xufei_zongshu') + 1, - xufei_shouyi=F('xufei_shouyi') + xufei_jine + ManagerRenewalDailyStats.query.filter(id=stat.id).update( + RenewalTotal=F('RenewalTotal') + 1, + RenewalRevenue=F('RenewalRevenue') + xufei_jine ) @@ -370,39 +313,39 @@ def update_zuzhang_daily_by_action(yonghuid, action, amount=Decimal('0.00')): today = date.today() with transaction.atomic(): - stat, created = ZuzhangRiTongji.objects.select_for_update().get_or_create( - yonghuid=yonghuid, - riqi=today, + stat, created = LeaderDailyStats.objects.select_for_update().get_or_create( + LeaderID=yonghuid, + Date=today, defaults={ - 'yonghuid': yonghuid, - 'riqi': today, - 'yaoqing_guanshi_shu': 0, - 'shouru_zonge': Decimal('0.00'), - 'fenyong_jine': Decimal('0.00'), + 'LeaderID': yonghuid, + 'Date': today, + 'InvitedManagerCount': 0, + 'TotalIncome': Decimal('0.00'), + 'CommissionAmount': Decimal('0.00'), } ) if created: # 新记录直接设置初始值 if action == 1: - stat.yaoqing_guanshi_shu = 1 + stat.InvitedManagerCount = 1 elif action == 2: amount = Decimal(str(amount)) if amount else Decimal('0.00') - stat.shouru_zonge = amount - stat.fenyong_jine = amount + stat.TotalIncome = amount + stat.CommissionAmount = amount stat.save() else: # 已存在记录,使用 F 表达式原子累加 if action == 1: - ZuzhangRiTongji.query.filter(id=stat.id).update( - yaoqing_guanshi_shu=F('yaoqing_guanshi_shu') + 1 + LeaderDailyStats.query.filter(id=stat.id).update( + InvitedManagerCount=F('InvitedManagerCount') + 1 ) elif action == 2: amount = Decimal(str(amount)) if amount else Decimal('0.00') if amount > 0: - ZuzhangRiTongji.query.filter(id=stat.id).update( - shouru_zonge=F('shouru_zonge') + amount, - fenyong_jine=F('fenyong_jine') + amount + LeaderDailyStats.query.filter(id=stat.id).update( + TotalIncome=F('TotalIncome') + amount, + CommissionAmount=F('CommissionAmount') + amount ) @@ -415,13 +358,13 @@ def update_tixian_daily_stat(leixing, amount): """ today = date.today() with transaction.atomic(): - stat, created = TixianRiTongji.objects.select_for_update().get_or_create( - riqi=today, - leixing=leixing, + stat, created = WithdrawalDailyStats.objects.select_for_update().get_or_create( + Date=today, + WithdrawalType=leixing, defaults={'total_amount': amount} ) if not created: - TixianRiTongji.query.filter(id=stat.id).update( + WithdrawalDailyStats.query.filter(id=stat.id).update( total_amount=F('total_amount') + amount ) diff --git a/backend/view.py b/backend/view.py index 01bfb6b..bd7edf2 100644 --- a/backend/view.py +++ b/backend/view.py @@ -52,7 +52,7 @@ from .utils import ( # models 集中导入 ## backend -from backend.models import Role, Permission, RolePermission, UserRole, TixianRiTongji +from backend.models import Role, Permission, RolePermission, UserRole, WithdrawalDailyStats ## users from users.models import ( @@ -3329,10 +3329,10 @@ class GetWithdrawSettingsView(APIView): # 4. 当日已提现总额(按提现类型 leixing 1~6) today = date.today() today_totals = {i: 0.0 for i in range(1, 7)} - records = TixianRiTongji.query.filter(riqi=today) + records = WithdrawalDailyStats.query.filter(Date=today) for rec in records: - if rec.leixing in today_totals: - today_totals[rec.leixing] = float(rec.total_amount) + if rec.WithdrawalType in today_totals: + today_totals[rec.WithdrawalType] = float(rec.total_amount) # 🆕 获取提现模式(自动1 / 手动2),默认2 config = WithdrawConfig.query.filter(id=1).first() @@ -3358,7 +3358,7 @@ class UpdateWithdrawSettingsView(APIView): - 修改管事相关需要5500b - 修改组长相关需要5500c 注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算), - 后端不直接修改 TixianRiTongji,该表由提现流程自动累加。 + 后端不直接修改 WithdrawalDailyStats,该表由提现流程自动累加。 POST /houtai/htxgtxsz """ permission_classes = [] @@ -3491,7 +3491,7 @@ class UpdateWithdrawSettingsView(APIView): defaults={'mode': int(withdraw_mode)} ) - # 注意:TixianRiTongji 表由提现流程自动更新,此处不直接修改 + # 注意:WithdrawalDailyStats 表由提现流程自动更新,此处不直接修改 return Response({'code': 0, 'msg': '设置修改成功'}) diff --git a/gvsdsdk/fluent.py b/gvsdsdk/fluent.py index f6d007e..a25acea 100644 --- a/gvsdsdk/fluent.py +++ b/gvsdsdk/fluent.py @@ -1,149 +1,240 @@ +"""FluentQuery 兼容层查询构建器 + +提供 SQLAlchemy 风格的链式 OOP 查询接口,包装 Django QuerySet。 + +核心类: +- ``FluentQuery`` — 链式查询构建器,由 ``Model.query`` 返回 +- ``Session`` — 简易会话管理(add/commit/rollback) +- ``_CompatFunc`` — 兼容层聚合函数(func.sum / func.count 等) +- ``db`` — 全局数据库操作入口(db.session / db.or_ / db.and_) + +典型用法:: + + from gvsdsdk.fluent import db, func, FQ + + # 链式查询 + orders = Dingdan.query.filter(zhuangtai=8).order_by('-create_time').to_list() + + # 字段表达式 + Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1,2,3])) + + # 聚合 + Dingdan.query.aggregate(total=Sum('jine')) + + # values / annotate + Dingdan.query.values('zhuangtai').annotate(cnt=Count('id')) + + # 分页 + Dingdan.query.paginate(page=1, per_page=20) + + # 兼容层聚合函数 + func.sum(Dingdan.jine) + func.count(Dingdan.id) + func.year(Dingdan.create_time) +""" from __future__ import annotations +import logging import threading -import functools -from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union, overload +from typing import Any, Dict, Generic, Iterator, List, Optional, Sequence, Tuple, Type, TypeVar, Union from django.db import connection as default_db_connection, models, transaction from django.db.models import Q, F, Count, Sum, Avg, Min, Max, Subquery, OuterRef, Case, When, Value +logger = logging.getLogger(__name__) _local = threading.local() +#: 模型类型变量,用于 FluentQuery[_M] 泛型参数 +_M = TypeVar('_M', bound=models.Model) + def _get_field_expression_cls(): - """延迟导入,避免 fluent ↔ model_base 循环依赖""" + """延迟导入 FieldExpression,避免 fluent ↔ model_base 循环依赖。""" from gvsdsdk.model_base import FieldExpression return FieldExpression +# --------------------------------------------------------------------------- +# 兼容层聚合函数 +# --------------------------------------------------------------------------- + class _AggregateExpr: - def __init__(self, model, aggregate): + """聚合表达式包装,关联模型类和 Django Aggregate 对象。""" + + def __init__(self, model: Optional[Type[models.Model]], aggregate: models.Aggregate) -> None: self.model = model self.aggregate = aggregate class _ExtractExpr: - def __init__(self, field_name, lookup_type): + """日期提取表达式(year/month/day),用于 ``func.year(field) == 2024`` 等。""" + + def __init__(self, field_name: str, lookup_type: str) -> None: self.field_name = field_name self.lookup_type = lookup_type - def __eq__(self, other): + def __eq__(self, other) -> Q: # type: ignore[override] return Q(**{f'{self.field_name}__{self.lookup_type}': other}) - def __and__(self, other): + def __and__(self, other) -> Q: if isinstance(other, Q): return Q(**{f'{self.field_name}__{self.lookup_type}': None}) & other return self.__eq__(other) class _CompatFunc: - def sum(self, field_expr): - if hasattr(field_expr, '_django_name'): - agg = Sum(field_expr._django_name) - elif hasattr(field_expr, 'field_name'): - agg = Sum(field_expr.field_name) - else: - agg = Sum(str(field_expr)) - model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + """兼容层聚合函数集合,提供 SQLAlchemy 风格的 ``func.sum()`` / ``func.count()`` 等。 + + 用法:: + + from gvsdsdk.fluent import func + + func.sum(Model.jine) → Sum('jine') + func.count(Model.id) → Count('id') + func.max(Model.jine) → Max('jine') + func.min(Model.jine) → Min('jine') + func.avg(Model.jine) → Avg('jine') + func.year(Model.riqi) → _ExtractExpr('riqi', 'year') + func.month(Model.riqi) → _ExtractExpr('riqi', 'month') + func.day(Model.riqi) → _ExtractExpr('riqi', 'day') + """ + + def sum(self, field_expr) -> Union[_AggregateExpr, Sum]: + """求和聚合。""" + name = self._resolve_name(field_expr) + agg = Sum(name) + model = getattr(field_expr, 'model_class', None) return _AggregateExpr(model, agg) if model else agg - def count(self, field_expr=None): + def count(self, field_expr=None) -> Union[_AggregateExpr, Count]: + """计数聚合,无参数时等价于 COUNT(*)。""" if field_expr is None: return Count('*') - if hasattr(field_expr, '_django_name'): - agg = Count(field_expr._django_name) - else: - agg = Count(str(field_expr)) - model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + name = self._resolve_name(field_expr) + agg = Count(name) + model = getattr(field_expr, 'model_class', None) return _AggregateExpr(model, agg) if model else agg - def max(self, field_expr): - if hasattr(field_expr, '_django_name'): - agg = Max(field_expr._django_name) - else: - agg = Max(str(field_expr)) - model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + def max(self, field_expr) -> Union[_AggregateExpr, Max]: + """最大值聚合。""" + name = self._resolve_name(field_expr) + agg = Max(name) + model = getattr(field_expr, 'model_class', None) return _AggregateExpr(model, agg) if model else agg - def min(self, field_expr): - if hasattr(field_expr, '_django_name'): - agg = Min(field_expr._django_name) - else: - agg = Min(str(field_expr)) - model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + def min(self, field_expr) -> Union[_AggregateExpr, Min]: + """最小值聚合。""" + name = self._resolve_name(field_expr) + agg = Min(name) + model = getattr(field_expr, 'model_class', None) return _AggregateExpr(model, agg) if model else agg - def avg(self, field_expr): - if hasattr(field_expr, '_django_name'): - agg = Avg(field_expr._django_name) - else: - agg = Avg(str(field_expr)) - model = getattr(field_expr, 'model_class', None) if hasattr(field_expr, 'model_class') else None + def avg(self, field_expr) -> Union[_AggregateExpr, Avg]: + """平均值聚合。""" + name = self._resolve_name(field_expr) + agg = Avg(name) + model = getattr(field_expr, 'model_class', None) return _AggregateExpr(model, agg) if model else agg - def year(self, field_expr): - if hasattr(field_expr, '_django_name'): - return _ExtractExpr(field_expr._django_name, 'year') - return _ExtractExpr(str(field_expr), 'year') + def year(self, field_expr) -> _ExtractExpr: + """提取年份,用于 ``func.year(Model.date) == 2024``。""" + return _ExtractExpr(self._resolve_name(field_expr), 'year') - def month(self, field_expr): - if hasattr(field_expr, '_django_name'): - return _ExtractExpr(field_expr._django_name, 'month') - return _ExtractExpr(str(field_expr), 'month') + def month(self, field_expr) -> _ExtractExpr: + """提取月份。""" + return _ExtractExpr(self._resolve_name(field_expr), 'month') - def day(self, field_expr): + def day(self, field_expr) -> _ExtractExpr: + """提取日期。""" + return _ExtractExpr(self._resolve_name(field_expr), 'day') + + @staticmethod + def _resolve_name(field_expr) -> str: + """从 FieldExpression 或字符串解析 Django 字段名。""" if hasattr(field_expr, '_django_name'): - return _ExtractExpr(field_expr._django_name, 'day') - return _ExtractExpr(str(field_expr), 'day') + return field_expr._django_name + if hasattr(field_expr, 'field_name'): + return field_expr.field_name + return str(field_expr) func = _CompatFunc() +# --------------------------------------------------------------------------- +# DELETE 语句构建器 +# --------------------------------------------------------------------------- + class _DeleteStmt: - def __init__(self, model_class, conditions=None): + """DELETE 语句构建器,支持 ``Model.query.filter(...).delete()`` 风格。""" + + def __init__(self, model_class: Type[models.Model], conditions: Optional[Q] = None) -> None: self.model_class = model_class self.conditions = conditions or Q() - def where(self, *args, **kwargs): + def where(self, *args: Q, **kwargs: Any) -> _DeleteStmt: + """添加 WHERE 条件。""" new_conditions = Q() for a in args: if isinstance(a, Q): new_conditions &= a + if kwargs: + new_conditions &= Q(**kwargs) self.conditions = new_conditions return self - def execute(self): + def execute(self) -> Tuple[int, Dict[str, int]]: + """执行删除,返回 (删除数, {表名: 删除数})。""" if self.model_class and self.conditions: - self.model_class.objects.filter(self.conditions).delete() + return self.model_class.objects.filter(self.conditions).delete() elif self.model_class: - self.model_class.objects.all().delete() + return self.model_class.objects.all().delete() + return (0, {}) -def _get_session(): +# --------------------------------------------------------------------------- +# 会话与缓存 +# --------------------------------------------------------------------------- + +def _get_session() -> Session: + """获取当前线程的 Session 实例。""" if not hasattr(_local, 'db_session'): _local.db_session = Session() return _local.db_session -def _get_cache(): +def _get_cache() -> Dict: + """获取当前线程的查询缓存。""" if not hasattr(_local, 'query_cache'): _local.query_cache = {} return _local.query_cache -def _invalidate_cache(): +def _invalidate_cache() -> None: + """清除当前线程的查询缓存。""" if hasattr(_local, 'query_cache'): _local.query_cache.clear() +# --------------------------------------------------------------------------- +# 关联字段访问器 +# --------------------------------------------------------------------------- + class RelationAccessor: - def __init__(self, model_class, prefix=''): + """关联字段访问器,支持 ``Model.relation.field`` 跨表字段表达式。 + + 用法:: + + # 自动解析关联字段 + Dingdan.query.filter(Dingdan.shangjia.nicheng == '测试') + """ + + def __init__(self, model_class: Type[models.Model], prefix: str = '') -> None: self._model = model_class self._prefix = prefix - def __getattr__(self, name): + def __getattr__(self, name: str): if name.startswith('_'): return object.__getattribute__(self, name) try: @@ -163,18 +254,26 @@ class RelationAccessor: return RelationAccessor(rel.related_model, full_prefix) return _get_field_expression_cls()(self._model, name) + def __repr__(self) -> str: + return f'RelationAccessor({self._model.__name__}, prefix={self._prefix!r})' + + +# --------------------------------------------------------------------------- +# Values 结果集 +# --------------------------------------------------------------------------- class _ValuesResult: + """包装 Django ValuesQuerySet,支持链式调用。 + + 底层使用 Django ``.values()`` 返回字典,保持与 Django 行为一致。 + 额外提供 ``.all()`` 返回字典列表,``.first()`` 返回字典或 None, + ``.scalar()`` 返回单字段场景的标量值。 """ - 包装 Django ValuesQuerySet,支持链式调用。 - 底层使用 Django .values() 返回字典,保持与 Django 行为一致。 - 额外提供 .all() 返回字典列表,.first() 返回字典或 None, - .scalar() 返回单字段场景的标量值。 - """ + def __init__(self, qs: models.QuerySet, fields: List[str], original_fields: Tuple) -> None: - self._qs: models.QuerySet = qs # Django ValuesQuerySet(返回字典) - self._fields: List[str] = fields # 转换后的字段名列表(传给 Django .values() 的) - self._original_fields: Tuple = original_fields # 原始传入的字段(用于判断单字段场景) + self._qs: models.QuerySet = qs + self._fields: List[str] = fields + self._original_fields: Tuple = original_fields @property def query(self) -> Any: @@ -202,12 +301,15 @@ class _ValuesResult: return self def first(self) -> Optional[Dict[str, Any]]: + """返回第一条记录(字典)或 None。""" return self._qs.first() def all(self) -> List[Dict[str, Any]]: + """返回所有记录的字典列表。""" return list(self._qs) def scalar(self) -> Any: + """返回单字段场景的标量值。""" row = self._qs.first() if row is None: return None @@ -237,11 +339,18 @@ class _ValuesResult: def __bool__(self) -> bool: return self._qs.exists() + def __repr__(self) -> str: + return f'_ValuesResult(fields={self._fields}, count={self.count()})' -class FluentQuery: + +# --------------------------------------------------------------------------- +# FluentQuery 核心查询构建器 +# --------------------------------------------------------------------------- + +class FluentQuery(Generic[_M]): """QModel 兼容层查询构建器,提供 SQLAlchemy 风格的链式 OOP 查询接口。 - 用法示例:: + 由 ``Model.query`` 返回,支持链式调用:: # 基础查询 Dingdan.query.filter(zhuangtai=8).all() @@ -249,7 +358,7 @@ class FluentQuery: # 链式过滤 Dingdan.query.filter(jine__gt=100).order_by('-create_time').limit(10) - # 字段表达式(通过 Model.field 自动获取 FieldExpression) + # 字段表达式 Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1,2,3])) # 聚合 @@ -260,10 +369,12 @@ class FluentQuery: # 分页 Dingdan.query.paginate(page=1, per_page=20) + + 泛型参数 ``_M`` 携带模型类型,使 IDE 能推断链式调用返回的模型实例类型。 """ - def __init__(self, model_class: Type[models.Model]) -> None: - self._model: Type[models.Model] = model_class + def __init__(self, model_class: Type[_M]) -> None: + self._model: Type[_M] = model_class self._qs: models.QuerySet = model_class.objects.all() self._joined_models: List[Type[models.Model]] = [] self._cache_enabled: bool = True @@ -273,7 +384,13 @@ class FluentQuery: """暴露底层 Django QuerySet 的 query 属性,支持 Subquery() 等场景。""" return self._qs.query - def _clone(self) -> FluentQuery: + @property + def model(self) -> Type[_M]: + """返回关联的模型类。""" + return self._model + + def _clone(self) -> FluentQuery[_M]: + """克隆当前查询状态。""" q = FluentQuery(self._model) q._qs = self._qs q._joined_models = list(self._joined_models) @@ -281,6 +398,7 @@ class FluentQuery: return q def _cache_key(self, method: str) -> Tuple[Any, str, Any]: + """生成缓存键。""" try: query_str = str(self._qs.query) except Exception: @@ -288,6 +406,7 @@ class FluentQuery: return (self._model, method, query_str) def _remap_q(self, q: Q) -> Q: + """重映射 Q 对象中的字段名(用于 join 跨表查询)。""" new_children: List[Any] = [] for child in q.children: if isinstance(child, Q): @@ -306,6 +425,7 @@ class FluentQuery: return q def _remap_key(self, key: str) -> str: + """重映射字段名(用于 join 跨表查询自动添加关联前缀)。""" parts = key.split('__') field_name = parts[0] try: @@ -330,7 +450,9 @@ class FluentQuery: pass return key - def filter(self, *args: Any, **kwargs: Any) -> FluentQuery: + # ---- 过滤 ---- + + def filter(self, *args: Any, **kwargs: Any) -> FluentQuery[_M]: """过滤查询,支持 Q 对象和关键字参数,返回 self 以支持链式调用。""" converted_args: List[Any] = [] for arg in args: @@ -341,7 +463,7 @@ class FluentQuery: self._qs = self._qs.filter(*converted_args, **kwargs) return self - def exclude(self, *args: Any, **kwargs: Any) -> FluentQuery: + def exclude(self, *args: Any, **kwargs: Any) -> FluentQuery[_M]: """排除查询,返回 self 以支持链式调用。""" converted_args: List[Any] = [] for arg in args: @@ -352,7 +474,9 @@ class FluentQuery: self._qs = self._qs.exclude(*converted_args, **kwargs) return self - def order_by(self, *args: Any) -> FluentQuery: + # ---- 排序 ---- + + def order_by(self, *args: Any) -> FluentQuery[_M]: """排序,支持字符串和 FieldExpression,返回 self 以支持链式调用。""" converted: List[str] = [] for a in args: @@ -365,7 +489,9 @@ class FluentQuery: self._qs = self._qs.order_by(*converted) return self - def first(self) -> Optional[models.Model]: + # ---- 获取记录 ---- + + def first(self) -> Optional[_M]: """返回第一条记录或 None。""" if self._cache_enabled: cache = _get_cache() @@ -379,13 +505,26 @@ class FluentQuery: _get_cache()[self._cache_key('first')] = obj return obj - def all(self) -> FluentQuery: + def last(self) -> Optional[_M]: + """返回最后一条记录或 None。""" + return self._qs.last() + + def get(self, *args: Any, **kwargs: Any) -> _M: + """获取唯一匹配记录,不存在或多个时抛异常。""" + if args: + obj = self._qs.filter(*args).get(**kwargs) + else: + obj = self._qs.get(**kwargs) + _get_session().track(obj) + return obj + + def all(self) -> FluentQuery[_M]: """返回 self 以支持链式调用(与 Django QuerySet.all() 一致)。 - 如需获取列表,使用 .to_list() 或 list()。""" + 如需获取列表,使用 ``.to_list()`` 或 ``list()``。""" self._qs = self._qs.all() return self - def to_list(self) -> List[models.Model]: + def to_list(self) -> List[_M]: """执行查询并返回模型实例列表。""" if self._cache_enabled: cache = _get_cache() @@ -412,93 +551,37 @@ class FluentQuery: _get_cache()[self._cache_key('count')] = result return result - def get(self, *args: Any, **kwargs: Any) -> models.Model: - """获取唯一匹配记录,不存在或多个时抛异常。""" - if args: - obj = self._qs.filter(*args).get(**kwargs) - else: - obj = self._qs.get(**kwargs) - _get_session().track(obj) - return obj + def exists(self) -> bool: + """判断是否存在匹配记录。""" + return self._qs.exists() - def create(self, **kwargs: Any) -> models.Model: + # ---- 创建/更新/删除 ---- + + def create(self, **kwargs: Any) -> _M: """创建并保存新记录。""" obj = self._model.objects.create(**kwargs) _invalidate_cache() return obj - def get_or_create(self, defaults: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Tuple[models.Model, bool]: - """查询或创建记录,返回 (obj, created) 元组。""" + def get_or_create(self, defaults: Optional[Dict[str, Any]] = None, + **kwargs: Any) -> Tuple[_M, bool]: + """查询或创建记录,返回 ``(obj, created)`` 元组。""" obj, created = self._model.objects.get_or_create(defaults=defaults, **kwargs) if created: _invalidate_cache() _get_session().track(obj) return obj, created - def update_or_create(self, defaults: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Tuple[models.Model, bool]: - """更新或创建记录,返回 (obj, created) 元组。""" + def update_or_create(self, defaults: Optional[Dict[str, Any]] = None, + **kwargs: Any) -> Tuple[_M, bool]: + """更新或创建记录,返回 ``(obj, created)`` 元组。""" obj, created = self._model.objects.update_or_create(defaults=defaults, **kwargs) _invalidate_cache() _get_session().track(obj) return obj, created - def none(self) -> FluentQuery: - """返回空结果集,返回 self 以支持链式调用。""" - self._qs = self._qs.none() - return self - - def last(self) -> Optional[models.Model]: - """返回最后一条记录或 None。""" - return self._qs.last() - - def limit(self, n: int) -> FluentQuery: - """限制返回记录数,返回 self 以支持链式调用。""" - self._qs = self._qs[:n] - return self - - def offset(self, n: int) -> FluentQuery: - """跳过前 n 条记录,返回 self 以支持链式调用。""" - self._qs = self._qs[n:] - return self - - def join(self, *args: Type[models.Model]) -> FluentQuery: - """关联模型,用于跨表查询自动映射字段,返回 self 以支持链式调用。""" - for arg in args: - if isinstance(arg, type) and issubclass(arg, models.Model): - if arg not in self._joined_models: - self._joined_models.append(arg) - return self - - def select_related(self, *fields: str) -> FluentQuery: - """外键关联查询优化,返回 self 以支持链式调用。""" - self._qs = self._qs.select_related(*fields) - return self - - def prefetch_related(self, *fields: str) -> FluentQuery: - """多对多/反向关联查询优化,返回 self 以支持链式调用。""" - self._qs = self._qs.prefetch_related(*fields) - return self - - def only(self, *fields: str) -> FluentQuery: - """只加载指定字段,返回 self 以支持链式调用。""" - self._qs = self._qs.only(*fields) - return self - - def defer(self, *fields: str) -> FluentQuery: - """延迟加载指定字段,返回 self 以支持链式调用。""" - self._qs = self._qs.defer(*fields) - return self - - def select_for_update(self, nowait: bool = False, skip_locked: bool = False, - of: Tuple = (), no_key: bool = False) -> FluentQuery: - """行级锁,返回 self 以支持链式调用。""" - self._qs = self._qs.select_for_update( - nowait=nowait, skip_locked=skip_locked, of=of, no_key=no_key - ) - return self - - def bulk_create(self, objs: List[models.Model], batch_size: Optional[int] = None, - ignore_conflicts: bool = False) -> List[models.Model]: + def bulk_create(self, objs: List[_M], batch_size: Optional[int] = None, + ignore_conflicts: bool = False) -> List[_M]: """批量创建记录。""" result = self._model.objects.bulk_create( objs, batch_size=batch_size, ignore_conflicts=ignore_conflicts @@ -506,7 +589,100 @@ class FluentQuery: _invalidate_cache() return result - def annotate(self, **kwargs: Any) -> FluentQuery: + def bulk_update(self, objs: List[_M], fields: List[str], + batch_size: Optional[int] = None) -> int: + """批量更新记录,返回受影响行数。""" + result = self._model.objects.bulk_update(objs, fields, batch_size=batch_size) + _invalidate_cache() + return result + + def update(self, **kwargs: Any) -> int: + """批量更新匹配记录,返回受影响行数。""" + result = self._qs.update(**kwargs) + _invalidate_cache() + return result + + def delete(self) -> Tuple[int, Dict[str, int]]: + """批量删除匹配记录,返回 ``(删除数, {表名: 删除数})``。""" + result = self._qs.delete() + _invalidate_cache() + return result + + # ---- 查询修饰 ---- + + def none(self) -> FluentQuery[_M]: + """返回空结果集,返回 self 以支持链式调用。""" + self._qs = self._qs.none() + return self + + def limit(self, n: int) -> FluentQuery[_M]: + """限制返回记录数,返回 self 以支持链式调用。""" + self._qs = self._qs[:n] + return self + + def offset(self, n: int) -> FluentQuery[_M]: + """跳过前 n 条记录,返回 self 以支持链式调用。""" + self._qs = self._qs[n:] + return self + + def join(self, *args: Type[models.Model]) -> FluentQuery[_M]: + """关联模型,用于跨表查询自动映射字段,返回 self 以支持链式调用。""" + for arg in args: + if isinstance(arg, type) and issubclass(arg, models.Model): + if arg not in self._joined_models: + self._joined_models.append(arg) + return self + + def select_related(self, *fields: str) -> FluentQuery[_M]: + """外键关联查询优化,返回 self 以支持链式调用。""" + self._qs = self._qs.select_related(*fields) + return self + + def prefetch_related(self, *fields: str) -> FluentQuery[_M]: + """多对多/反向关联查询优化,返回 self 以支持链式调用。""" + self._qs = self._qs.prefetch_related(*fields) + return self + + def only(self, *fields: str) -> FluentQuery[_M]: + """只加载指定字段,返回 self 以支持链式调用。""" + self._qs = self._qs.only(*fields) + return self + + def defer(self, *fields: str) -> FluentQuery[_M]: + """延迟加载指定字段,返回 self 以支持链式调用。""" + self._qs = self._qs.defer(*fields) + return self + + def select_for_update(self, nowait: bool = False, skip_locked: bool = False, + of: Tuple = (), no_key: bool = False) -> FluentQuery[_M]: + """行级锁,返回 self 以支持链式调用。""" + self._qs = self._qs.select_for_update( + nowait=nowait, skip_locked=skip_locked, of=of, no_key=no_key + ) + return self + + def distinct(self) -> FluentQuery[_M]: + """去重查询,返回 self 以支持链式调用。""" + self._qs = self._qs.distinct() + return self + + def using(self, alias: str) -> FluentQuery[_M]: + """指定数据库别名,返回 self 以支持链式调用。""" + self._qs = self._qs.using(alias) + return self + + def no_cache(self) -> FluentQuery[_M]: + """禁用查询缓存,返回 self 以支持链式调用。""" + self._cache_enabled = False + return self + + def params(self, **kwargs: Any) -> FluentQuery[_M]: + """参数占位(SQLAlchemy 兼容),返回 self 以支持链式调用。""" + return self + + # ---- 聚合与注解 ---- + + def annotate(self, **kwargs: Any) -> FluentQuery[_M]: """注解查询,添加聚合/计算字段,返回 self 以支持链式调用。""" self._qs = self._qs.annotate(**kwargs) return self @@ -518,7 +694,6 @@ class FluentQuery: def values(self, *fields: Union[str, _AggregateExpr, models.Expression]) -> _ValuesResult: """返回指定字段的字典结果,支持链式调用。""" if not fields: - # 无参数:返回 Django .values()(所有字段字典) return _ValuesResult(self._qs.values(), [], fields) annotations: Dict[str, models.Aggregate] = {} @@ -538,7 +713,6 @@ class FluentQuery: qs = self._qs if annotations: qs = qs.annotate(**annotations) - # 使用 Django 原生 .values() 返回字典 qs = qs.values(*converted) return _ValuesResult(qs, converted, fields) @@ -546,38 +720,16 @@ class FluentQuery: """values() 的别名,SQLAlchemy 风格。""" return self.values(*fields) - def params(self, **kwargs: Any) -> FluentQuery: - """参数占位(SQLAlchemy 兼容),返回 self 以支持链式调用。""" - return self - def values_list(self, *fields: str, flat: bool = False) -> Any: """返回指定字段的元组列表。""" return self._qs.values_list(*fields, flat=flat) - def update(self, **kwargs: Any) -> int: - """批量更新匹配记录,返回受影响行数。""" - result = self._qs.update(**kwargs) - _invalidate_cache() - return result + # ---- 分页 ---- - def delete(self) -> Tuple[int, Dict[str, int]]: - """批量删除匹配记录,返回 (删除数, {表名: 删除数})。""" - result = self._qs.delete() - _invalidate_cache() - return result - - def exists(self) -> bool: - """判断是否存在匹配记录。""" - return self._qs.exists() - - def distinct(self) -> FluentQuery: - """去重查询,返回 self 以支持链式调用。""" - self._qs = self._qs.distinct() - return self - - def paginate(self, page: int = 1, per_page: int = 20, error_out: bool = False) -> Any: + def paginate(self, page: int = 1, per_page: int = 20, + error_out: bool = False) -> Any: """分页查询,返回 Page 对象(含 items, total, pages 等属性)。""" - from django.core.paginator import Paginator + from django.core.paginator import Paginator, EmptyPage if not self._qs.ordered: meta = getattr(self._model, '_meta', None) if meta and meta.pk: @@ -585,7 +737,7 @@ class FluentQuery: paginator = Paginator(self._qs, per_page) try: page_obj = paginator.page(page) - except Exception: + except EmptyPage: page_obj = paginator.page(1) page_obj.total = paginator.count page_obj.items = page_obj.object_list @@ -598,26 +750,17 @@ class FluentQuery: page_obj.prev_num = page_obj.previous_page_number() if page_obj.has_prev else None return page_obj - def no_cache(self) -> FluentQuery: - """禁用查询缓存,返回 self 以支持链式调用。""" - self._cache_enabled = False - return self - - def using(self, alias: str) -> FluentQuery: - """指定数据库别名,返回 self 以支持链式调用。""" - self._qs = self._qs.using(alias) - return self + # ---- 魔术方法 ---- def __getitem__(self, key: Union[int, slice]) -> Any: - """支持切片 [start:end] 和索引 [n]。切片返回 self,索引返回模型实例。""" + """支持切片 ``[start:end]`` 和索引 ``[n]``。切片返回 self,索引返回模型实例。""" if isinstance(key, slice): self._qs = self._qs[key] return self else: - # 单个索引,返回模型实例 return self._qs[key] - def __iter__(self) -> Iterator[models.Model]: + def __iter__(self) -> Iterator[_M]: return iter(self._qs) def __len__(self) -> int: @@ -630,25 +773,43 @@ class FluentQuery: return f'FluentQuery({self._model.__name__})' -class Session: - def __init__(self): - self._pending_adds = [] - self._pending_deletes = [] - self._tracked = set() +# --------------------------------------------------------------------------- +# Session 会话管理 +# --------------------------------------------------------------------------- - def add(self, obj): +class Session: + """简易会话管理器,支持 add/delete/commit/rollback 操作。 + + 用法:: + + session = db.session + session.add(obj) + session.commit() + """ + + def __init__(self) -> None: + self._pending_adds: List[models.Model] = [] + self._pending_deletes: List[models.Model] = [] + self._tracked: set = set() + + def add(self, obj: models.Model) -> None: + """添加对象到待保存列表。""" self._pending_adds.append(obj) - def delete(self, obj): + def delete(self, obj: models.Model) -> None: + """添加对象到待删除列表。""" self._pending_deletes.append(obj) self._tracked.discard(obj) - def track(self, obj): + def track(self, obj: models.Model) -> None: + """跟踪对象变更(commit 时自动 save)。""" if isinstance(obj, models.Model): self._tracked.add(obj) - def commit(self): - delete_pks = {(obj.__class__, obj.pk) for obj in self._pending_deletes if obj.pk is not None} + def commit(self) -> None: + """提交所有待处理操作(删除 → 新增 → 更新)。""" + delete_pks = {(obj.__class__, obj.pk) for obj in self._pending_deletes + if obj.pk is not None} with transaction.atomic(): for obj in self._pending_deletes: obj.delete() @@ -660,21 +821,23 @@ class Session: try: obj.save() except Exception: - pass + logger.debug('Session.commit: skip tracked obj %s', obj, exc_info=True) self._pending_adds.clear() self._pending_deletes.clear() self._tracked.clear() _invalidate_cache() - def rollback(self): + def rollback(self) -> None: + """回滚所有待处理操作。""" self._pending_adds.clear() self._pending_deletes.clear() self._tracked.clear() _invalidate_cache() - def query(self, *args): - model = None - aggregates = [] + def query(self, *args) -> Optional[FluentQuery[Any]]: + """创建查询构建器,支持传入模型类或聚合表达式。""" + model: Optional[Type[models.Model]] = None + aggregates: List = [] for a in args: if isinstance(a, type) and issubclass(a, models.Model): model = a @@ -691,10 +854,12 @@ class Session: return fq.values(*args) return None - def flush(self): + def flush(self) -> None: + """flush() 的别名,等同于 commit()。""" self.commit() - def execute(self, stmt): + def execute(self, stmt) -> None: + """执行语句(支持 _DeleteStmt 和原始 SQL 字符串)。""" if isinstance(stmt, _DeleteStmt): stmt.execute() elif isinstance(stmt, str): @@ -703,14 +868,31 @@ class Session: cursor.execute(stmt) +# --------------------------------------------------------------------------- +# 全局入口 +# --------------------------------------------------------------------------- + class DB: + """全局数据库操作入口。 + + 用法:: + + from gvsdsdk.fluent import db + + session = db.session + q = db.or_(Model.field == 1, Model.field == 2) + q = db.and_(Model.field > 0, Model.field < 100) + """ + @property - def session(self): + def session(self) -> Session: + """获取当前线程的 Session 实例。""" return _get_session() @staticmethod - def or_(*args): - result = None + def or_(*args: Q) -> Q: + """逻辑 OR,合并多个 Q 对象。""" + result: Optional[Q] = None for q in args: if isinstance(q, Q): if result is None: @@ -720,7 +902,8 @@ class DB: return result if result is not None else Q() @staticmethod - def and_(*args): + def and_(*args: Q) -> Q: + """逻辑 AND,合并多个 Q 对象。""" result = Q() for q in args: if isinstance(q, Q): @@ -731,5 +914,6 @@ class DB: db = DB() -def FQ(model_class): +def FQ(model_class: Type[_M]) -> FluentQuery[_M]: + """FluentQuery 工厂函数,等同于 ``Model.query``。""" return FluentQuery(model_class) diff --git a/gvsdsdk/model_base.py b/gvsdsdk/model_base.py index 9170247..49f567e 100644 --- a/gvsdsdk/model_base.py +++ b/gvsdsdk/model_base.py @@ -1,58 +1,81 @@ +"""QModel 兼容层基类模块 + +提供 SQLAlchemy 风格的链式 OOP 查询接口,使 Django ORM 支持: + +- ``Model.query.filter(...)`` — 链式 FluentQuery 查询 +- ``Model.field_name`` — 类级别返回 FieldExpression(支持 ``==``, ``>``, ``in_()`` 等运算符) +- ``Model.field_name`` — 实例级别返回字段值(与原生 Django Model 一致) +- ``Model[condition]`` — 快捷 filter 语法 + +典型用法:: + + # 链式查询 + orders = Dingdan.query.filter(zhuangtai=8).order_by('-create_time').to_list() + + # 字段表达式 + Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1, 2, 3])) + + # 聚合 + Dingdan.query.aggregate(total=Sum('jine')) + + # 分页 + Dingdan.query.paginate(page=1, per_page=20) +""" from __future__ import annotations -import typing -from typing import Any, Dict, List, Optional, Tuple, Type, Union +import re +from typing import Any, Callable, Dict, Generic, Optional, Self, Tuple, Type, TypeVar, Union from django.db import models from django.db.models import Q + from .fluent import FluentQuery -class classproperty: - def __init__(self, func): +# --------------------------------------------------------------------------- +# 工具函数 +# --------------------------------------------------------------------------- + +#: classproperty 返回值类型变量 +_RT = TypeVar('_RT') + +#: QModel 子类类型变量(仅用于 __class_getitem__ 兼容) +_QM = TypeVar('_QM', bound='QModel') + + +class classproperty(Generic[_RT]): + """类属性描述符,支持 ``Model.query`` 等类级别属性访问。 + + 泛型参数 ``_RT`` 传播函数返回类型,使 Pylance 能穿透描述符推断 + ``Model.query`` 的返回类型为 ``FluentQuery[Self]``。 + """ + + def __init__(self, func: Callable[[type], _RT]) -> None: self.fget = func - def __get__(self, obj, objtype=None): + def __get__(self, obj: Any, objtype: Optional[type] = None) -> _RT: return self.fget(objtype if objtype is not None else type(obj)) -ABBREVIATIONS = { - 'uuid': 'UUID', - 'id': 'ID', - 'ip': 'IP', - 'url': 'URL', - 'uri': 'URI', - 'api': 'API', - 'html': 'HTML', - 'css': 'CSS', - 'js': 'JS', - 'json': 'JSON', - 'xml': 'XML', - 'sql': 'SQL', - 'http': 'HTTP', - 'https': 'HTTPS', - 'smtp': 'SMTP', - 'ssl': 'SSL', - 'tls': 'TLS', - 'ssh': 'SSH', - 'ftp': 'FTP', - '2fa': '2FA', - 'elt': 'ELT', - 'sve': 'SVE', - 't4': 'T4', - 'mc': 'MC', - 'crb': 'CRB', - 'ccgo': 'CCGO', - 'ugm': 'UGM', - 'gerp': 'GERP', - 'abac': 'ABAC', - 'rbac': 'RBAC', - 'smtm': 'SMTM', - 'pwd': 'PWD', +ABBREVIATIONS: Dict[str, str] = { + 'uuid': 'UUID', 'id': 'ID', 'ip': 'IP', 'url': 'URL', 'uri': 'URI', + 'api': 'API', 'html': 'HTML', 'css': 'CSS', 'js': 'JS', 'json': 'JSON', + 'xml': 'XML', 'sql': 'SQL', 'http': 'HTTP', 'https': 'HTTPS', + 'smtp': 'SMTP', 'ssl': 'SSL', 'tls': 'TLS', 'ssh': 'SSH', 'ftp': 'FTP', + '2fa': '2FA', 'elt': 'ELT', 'sve': 'SVE', 't4': 'T4', 'mc': 'MC', + 'crb': 'CRB', 'ccgo': 'CCGO', 'ugm': 'UGM', 'gerp': 'GERP', + 'abac': 'ABAC', 'rbac': 'RBAC', 'smtm': 'SMTM', 'pwd': 'PWD', } -def snake_to_pascal(name): +def snake_to_pascal(name: str) -> str: + """将 snake_case 转换为 PascalCase,自动识别常见缩写。 + + >>> snake_to_pascal('user_uuid') + 'UserUUID' + >>> snake_to_pascal('paifa_dingdan_shu') + 'PaifaDingdanShu' + """ if '_' not in name: return name[0].upper() + name[1:] if name else name parts = name.split('_') @@ -66,46 +89,74 @@ def snake_to_pascal(name): return ''.join(result) -def pascal_to_snake(name): - import re +def pascal_to_snake(name: str) -> str: + """将 PascalCase 转换为 snake_case。 + + >>> pascal_to_snake('UserUUID') + 'user_uuid' + """ s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1) - parts = s2.lower().split('_') - return '_'.join(parts) + return s2.lower() +# --------------------------------------------------------------------------- +# 字段表达式(类级别访问 Model.field 时返回) +# --------------------------------------------------------------------------- + class _InvertedFieldExpression: - def __init__(self, field_expr): + """取反字段表达式,由 ``~Model.field`` 产生,支持 ``~field.startswith(...)`` 等否定查询。""" + + def __init__(self, field_expr: FieldExpression) -> None: self._field_expr = field_expr - def startswith(self, value): + def startswith(self, value: str) -> Q: return ~Q(**{f'{self._field_expr._django_name}__startswith': value}) - def contains(self, value): + def contains(self, value: str) -> Q: return ~Q(**{f'{self._field_expr._django_name}__contains': value}) - def like(self, pattern): + def like(self, pattern: str) -> Q: clean = pattern.strip('%') return ~Q(**{f'{self._field_expr._django_name}__icontains': clean}) - def in_(self, values): + def in_(self, values) -> Q: return ~Q(**{f'{self._field_expr._django_name}__in': values}) - def is_(self, other): + def is_(self, other) -> Q: if other is None: return ~Q(**{f'{self._field_expr._django_name}__isnull': True}) return ~Q(**{self._field_expr._django_name: other}) + def between(self, low, high) -> Q: + return ~(Q(**{f'{self._field_expr._django_name}__gte': low}) + & Q(**{f'{self._field_expr._django_name}__lte': high})) + + def __repr__(self) -> str: + return f'~{self._field_expr!r}' + class FieldExpression: - _resolve_cache = {} + """字段表达式,由类级别 ``Model.field_name`` 访问产生。 - def __init__(self, model_class, field_name): + 支持通过 Python 运算符构建 Django Q 对象:: + + Model.field == value → Q(field=value) + Model.field > value → Q(field__gt=value) + Model.field.in_([1,2,3]) → Q(field__in=[1,2,3]) + Model.field.between(a, b) → Q(field__gte=a) & Q(field__lte=b) + ~Model.field → _InvertedFieldExpression(否定查询) + """ + + _resolve_cache: Dict[Tuple[str, str], str] = {} + + def __init__(self, model_class: Type, field_name: str) -> None: self.model_class = model_class self.field_name = field_name - self._django_name = self._resolve_django_name() + self._django_name: str = self._resolve_django_name() - def _resolve_django_name(self): + def _resolve_django_name(self) -> str: + """将 Python 字段名解析为 Django ORM 查询使用的字段名。""" cache_key = (self.model_class.__name__, self.field_name) if cache_key in FieldExpression._resolve_cache: return FieldExpression._resolve_cache[cache_key] @@ -117,243 +168,232 @@ class FieldExpression: FieldExpression._resolve_cache[cache_key] = self.field_name return self.field_name - def __eq__(self, other): + # ---- 比较运算符 ---- + + def __eq__(self, other) -> Q: # type: ignore[override] if isinstance(other, FieldExpression): return NotImplemented if other is None: return Q(**{f'{self._django_name}__isnull': True}) return Q(**{self._django_name: other}) - def __ne__(self, other): + def __ne__(self, other) -> Q: # type: ignore[override] if isinstance(other, FieldExpression): return NotImplemented return ~Q(**{self._django_name: other}) - def __gt__(self, other): + def __gt__(self, other) -> Q: return Q(**{f'{self._django_name}__gt': other}) - def __ge__(self, other): + def __ge__(self, other) -> Q: return Q(**{f'{self._django_name}__gte': other}) - def __lt__(self, other): + def __lt__(self, other) -> Q: return Q(**{f'{self._django_name}__lt': other}) - def __le__(self, other): + def __le__(self, other) -> Q: return Q(**{f'{self._django_name}__lte': other}) - def __getitem__(self, key): - return _JsonKeyExpression(self._django_name, key) - - def __hash__(self): + def __hash__(self) -> int: return hash((self.model_class.__name__, self.field_name)) - def __and__(self, other): + # ---- 逻辑运算符 ---- + + def __and__(self, other) -> Q: if isinstance(other, Q): return Q(self) & other + if isinstance(other, FieldExpression): + return Q(self) & Q(other) return NotImplemented - def __or__(self, other): + def __or__(self, other) -> Q: if isinstance(other, Q): return Q(self) | other + if isinstance(other, FieldExpression): + return Q(self) | Q(other) return NotImplemented - def __invert__(self): + def __invert__(self) -> _InvertedFieldExpression: return _InvertedFieldExpression(self) - def is_(self, other): + # ---- 查询方法 ---- + + def is_(self, other) -> Q: + """``field IS NULL`` 或 ``field IS value``。""" if other is None: return Q(**{f'{self._django_name}__isnull': True}) return Q(**{self._django_name: other}) - def is_not(self, other): + def is_not(self, other) -> Q: + """``field IS NOT NULL`` 或 ``field IS NOT value``。""" if other is None: return Q(**{f'{self._django_name}__isnull': False}) return ~Q(**{self._django_name: other}) - def like(self, pattern): + def like(self, pattern: str) -> Q: + """模糊匹配(不区分大小写),自动去除 ``%`` 通配符。""" clean = pattern.strip('%') return Q(**{f'{self._django_name}__icontains': clean}) - def contains(self, value): + def contains(self, value: str) -> Q: + """包含子串(区分大小写)。""" return Q(**{f'{self._django_name}__contains': value}) - def ilike(self, pattern): + def ilike(self, pattern: str) -> Q: + """like() 的别名。""" return self.like(pattern) - def startswith(self, value): + def startswith(self, value: str) -> Q: + """前缀匹配(区分大小写)。""" return Q(**{f'{self._django_name}__startswith': value}) - def istartswith(self, value): + def istartswith(self, value: str) -> Q: + """前缀匹配(不区分大小写)。""" return Q(**{f'{self._django_name}__istartswith': value}) - def endswith(self, value): + def endswith(self, value: str) -> Q: + """后缀匹配(区分大小写)。""" return Q(**{f'{self._django_name}__endswith': value}) - def iendswith(self, value): + def iendswith(self, value: str) -> Q: + """后缀匹配(不区分大小写)。""" return Q(**{f'{self._django_name}__iendswith': value}) - def in_(self, values): + def in_(self, values) -> Q: + """``field IN (values)``,支持列表和 FluentQuery 子查询。""" if hasattr(values, '_qs'): return Q(**{f'{self._django_name}__in': values._qs.values(self._django_name)}) return Q(**{f'{self._django_name}__in': values}) - def between(self, low, high): + def between(self, low, high) -> Q: + """``field BETWEEN low AND high``。""" return Q(**{f'{self._django_name}__gte': low}) & Q(**{f'{self._django_name}__lte': high}) - def regex(self, pattern): + def regex(self, pattern: str) -> Q: + """正则匹配(区分大小写)。""" return Q(**{f'{self._django_name}__regex': pattern}) - def iregex(self, pattern): + def iregex(self, pattern: str) -> Q: + """正则匹配(不区分大小写)。""" return Q(**{f'{self._django_name}__iregex': pattern}) - def desc(self): + # ---- 排序辅助 ---- + + def desc(self) -> str: + """返回降序排序字符串 ``'-field_name'``。""" return f'-{self._django_name}' - def asc(self): + def asc(self) -> str: + """返回升序排序字符串 ``'field_name'``。""" return self._django_name - def __repr__(self): + # ---- JSON 字段访问 ---- + + def __getitem__(self, key: str) -> _JsonKeyExpression: + """访问 JSON 字段内的键,如 ``Model.meta['key'] == value``。""" + return _JsonKeyExpression(self._django_name, key) + + # ---- 显示 ---- + + def __repr__(self) -> str: return f'FieldExpr({self.model_class.__name__}.{self.field_name})' - def __str__(self): + def __str__(self) -> str: return self._django_name class _JsonKeyExpression: - def __init__(self, field_name, key): + """JSON 字段键表达式,由 ``Model.json_field['key']`` 产生。""" + + def __init__(self, field_name: str, key: str) -> None: self.field_name = field_name self.key = key - def _lookup(self, suffix=''): + def _lookup(self, suffix: str = '') -> str: if suffix: return f'{self.field_name}__{self.key}__{suffix}' return f'{self.field_name}__{self.key}' - def cast(self, type_hint): + def cast(self, type_hint) -> _CastJsonExpression: + """将 JSON 值转换为指定类型后再比较。""" return _CastJsonExpression(self.field_name, self.key, type_hint) - def __eq__(self, other): + def __eq__(self, other) -> Q: # type: ignore[override] return Q(**{self._lookup(): other}) - def __ne__(self, other): + def __ne__(self, other) -> Q: # type: ignore[override] return ~Q(**{self._lookup(): other}) - def __gt__(self, other): + def __gt__(self, other) -> Q: return Q(**{self._lookup('gt'): other}) - def __ge__(self, other): + def __ge__(self, other) -> Q: return Q(**{self._lookup('gte'): other}) - def __lt__(self, other): + def __lt__(self, other) -> Q: return Q(**{self._lookup('lt'): other}) - def __le__(self, other): + def __le__(self, other) -> Q: return Q(**{self._lookup('lte'): other}) - def between(self, low, high): + def between(self, low, high) -> Q: return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high}) - def __repr__(self): + def __repr__(self) -> str: return f'JsonKeyExpr({self.field_name}[{self.key!r}])' class _CastJsonExpression: - def __init__(self, field_name, key, type_hint): + """类型转换后的 JSON 键表达式。""" + + def __init__(self, field_name: str, key: str, type_hint) -> None: self.field_name = field_name self.key = key self.type_hint = type_hint - def _lookup(self, suffix=''): + def _lookup(self, suffix: str = '') -> str: if suffix: return f'{self.field_name}__{self.key}__{suffix}' return f'{self.field_name}__{self.key}' - def between(self, low, high): - return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high}) - - def __eq__(self, other): + def __eq__(self, other) -> Q: # type: ignore[override] return Q(**{self._lookup(): other}) - def __ne__(self, other): + def __ne__(self, other) -> Q: # type: ignore[override] return ~Q(**{self._lookup(): other}) - def __gt__(self, other): + def __gt__(self, other) -> Q: return Q(**{self._lookup('gt'): other}) - def __ge__(self, other): + def __ge__(self, other) -> Q: return Q(**{self._lookup('gte'): other}) - def __lt__(self, other): + def __lt__(self, other) -> Q: return Q(**{self._lookup('lt'): other}) - def __le__(self, other): + def __le__(self, other) -> Q: return Q(**{self._lookup('lte'): other}) - def __repr__(self): + def between(self, low, high) -> Q: + return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high}) + + def __repr__(self) -> str: type_name = getattr(self.type_hint, '__name__', str(self.type_hint)) return f'CastJsonExpr({self.field_name}[{self.key!r}]->{type_name})' -class ForeignKeyIdDescriptor: - """FK 字段描述符:类级别访问返回 FieldExpression,实例级别访问/设置委托给原始 Django 字段。""" - def __init__(self, fk_name): - self.fk_name = fk_name - self.id_attr = fk_name + '_id' - self._original_field = None # 延迟绑定原始 Django 字段 - - def _get_original_field(self, owner): - if self._original_field is None: - try: - self._original_field = owner._meta.get_field(self.fk_name) - except Exception: - pass - return self._original_field - - def __get__(self, obj, objtype=None): - if obj is None: - return FieldExpression(objtype, self.id_attr) - return getattr(obj, self.id_attr) - - def __set__(self, obj, value): - # 直接写入实例 __dict__,避免触发描述符递归 - obj.__dict__[self.id_attr] = value - - -class FieldExpressionDescriptor: - """普通字段描述符:类级别访问返回 FieldExpression,实例级别访问/设置委托给原始 Django 字段。""" - def __init__(self, field_name): - self.field_name = field_name - self._original_field = None # 延迟绑定原始 Django 字段 - - def _get_original_field(self, owner): - if self._original_field is None: - try: - self._original_field = owner._meta.get_field(self.field_name) - except Exception: - pass - return self._original_field - - def __get__(self, obj, objtype=None): - if obj is None: - return FieldExpression(objtype, self.field_name) - # 实例级别:从 __dict__ 获取,或让 Django 字段描述符处理 - if self.field_name in obj.__dict__: - return obj.__dict__[self.field_name] - # 委托给原始 Django 字段 - orig = self._get_original_field(objtype) - if orig is not None: - return orig.__get__(obj, objtype) - return getattr(obj, self.field_name) - - def __set__(self, obj, value): - # 直接写入实例 __dict__,避免触发描述符递归 - obj.__dict__[self.field_name] = value - +# --------------------------------------------------------------------------- +# QModel 元类与基类 +# --------------------------------------------------------------------------- class QModelBase(models.base.ModelBase): - def __new__(mcs, name, bases, namespace, **kwargs): - # 不再强制设置 db_column —— Django 会自动处理 + """QModel 元类,负责: + 1. 根据 ``GVSDSDK_MANAGE_TABLES`` 设置动态控制 ``managed`` + 2. 包装字段描述符,使类级别访问返回 FieldExpression + """ + + def __new__(mcs, name, bases, namespace, **kwargs): # 动态设置 managed:子服务器可设 GVSDSDK_MANAGE_TABLES=True 让 gvsdsdk 管理表 meta = namespace.get('Meta') if meta and getattr(meta, 'app_label', None) == 'gvsdsdk': @@ -378,12 +418,12 @@ def _wrap_field_descriptors(cls: Type) -> None: 实例级别完全委托给 Django 原生描述符。 Django 的字段描述符(DeferredAttribute / ForwardManyToOneDescriptor): - - __get__(None, cls) → 返回字段对象本身(类级别) - - __get__(instance, cls) → 返回字段值(实例级别) + - ``__get__(None, cls)`` → 返回字段对象本身(类级别) + - ``__get__(instance, cls)`` → 返回字段值(实例级别) 我们包装后: - - __get__(None, cls) → 返回 FieldExpression(类级别,用于链式 OOP) - - __get__(instance, cls) → 委托给原生描述符(实例级别,正常行为) + - ``__get__(None, cls)`` → 返回 FieldExpression(类级别,用于链式 OOP) + - ``__get__(instance, cls)`` → 委托给原生描述符(实例级别,正常行为) """ from django.db.models.query_utils import DeferredAttribute from django.db.models.fields.related_descriptors import ( @@ -408,13 +448,18 @@ def _wrap_field_descriptors(cls: Type) -> None: class _FieldExpressionWrapper: - """包装 Django 字段描述符,类级别返回 FieldExpression,实例级别委托给原生描述符。""" + """包装 Django 字段描述符,类级别返回 FieldExpression,实例级别委托给原生描述符。 - def __init__(self, original_descriptor, model_class, field_name): + 这是 QModel 字段表达式机制的核心:当你在类上访问 ``Model.field_name`` 时, + 返回 ``FieldExpression`` 对象,支持 ``Model.field == value`` 等运算符构建 Q 对象; + 当你在实例上访问 ``obj.field_name`` 时,行为与原生 Django 完全一致。 + """ + + def __init__(self, original_descriptor, model_class: Type, field_name: str) -> None: self._original = original_descriptor self._model = model_class self._field_name = field_name - self._has_set = hasattr(original_descriptor, '__set__') + self._has_set: bool = hasattr(original_descriptor, '__set__') def __get__(self, obj, objtype=None): if obj is None: @@ -423,7 +468,7 @@ class _FieldExpressionWrapper: # 实例级别访问:委托给原生 Django 描述符 return self._original.__get__(obj, objtype) - def __set__(self, obj, value): + def __set__(self, obj, value) -> None: if self._has_set: # FK/O2O 描述符有 __set__ self._original.__set__(obj, value) @@ -431,14 +476,19 @@ class _FieldExpressionWrapper: # 普通字段(DeferredAttribute)没有 __set__,直接写 __dict__ obj.__dict__[self._field_name] = value + def __repr__(self) -> str: + return f'_FieldExpressionWrapper({self._model.__name__}.{self._field_name})' + class QModel(models.Model, metaclass=QModelBase): """QModel 兼容层基类,提供 SQLAlchemy 风格的链式 OOP 查询接口。 所有继承 QModel 的模型自动获得: + - ``Model.query`` : 返回 FluentQuery,支持链式查询 - ``Model.field_name`` : 类级别访问返回 FieldExpression,支持表达式查询 - ``Model.field_name`` : 实例级别访问返回字段值(与普通 Django Model 一致) + - ``Model[condition]`` : 快捷 filter 语法 用法示例:: @@ -456,28 +506,30 @@ class QModel(models.Model, metaclass=QModelBase): # 分页 Dingdan.query.paginate(page=1, per_page=20) + + # 快捷 filter + Dingdan[Dingdan.zhuangtai == 8].order_by('-create_time').to_list() """ class Meta: abstract = True @classproperty - def query(cls) -> FluentQuery: + def query(cls) -> FluentQuery[Self]: """返回 FluentQuery 兼容层查询构建器,支持链式 OOP 查询。""" return FluentQuery(cls) @classmethod - def __class_getitem__(cls, item) -> FluentQuery: - """支持 Model[condition] 语法,返回 filter 后的 FluentQuery。 + def __class_getitem__(cls, item) -> FluentQuery[Self]: + """支持 ``Model[condition]`` 语法,返回 filter 后的 FluentQuery。 用法:: Dingdan[Dingdan.zhuangtai == 8].order_by('-create_time').to_list() + Dingdan[{'zhuangtai': 8}].to_list() """ if isinstance(item, Q): return cls.query.filter(item) if isinstance(item, dict): return cls.query.filter(**item) return cls.query.filter(item) - - diff --git a/gvsdsdk/models.py b/gvsdsdk/models.py index 47c0b03..afab338 100644 --- a/gvsdsdk/models.py +++ b/gvsdsdk/models.py @@ -174,7 +174,7 @@ class UserManager(BaseUserManager): class User(QModel): """用户表——系统核心身份模型(映射 SAAS 主库 User 表)""" - UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID', default=uuid.uuid4) + UserUUID = UUIDField(primary_key=True, db_column='UserUUID', verbose_name='用户UUID', default=_uuid.uuid4) UserName = models.CharField(max_length=256, unique=True, db_column='UserName', verbose_name='用户名') UserPassword = models.CharField(max_length=60, default='', db_column='UserPassword', verbose_name='密码') UserPositionStatus = models.SmallIntegerField(default=0, db_column='UserPositionStatus', verbose_name='职位状态') diff --git a/users/paihang_views.py b/users/paihang_views.py index 0233565..35fb2af 100644 --- a/users/paihang_views.py +++ b/users/paihang_views.py @@ -12,10 +12,10 @@ from rest_framework.response import Response from rest_framework.views import APIView from backend.models import ( - DashouRiTongji, - GuanshiRiTongji, - ZuzhangRiTongji, - ShangjiaRiTongji, + PlayerDailyStats, + ManagerDailyStats, + LeaderDailyStats, + MerchantDailyStats, ) from users.models import UserDashou, UserShangjia, UserBoss from gvsdsdk.models import User @@ -29,28 +29,32 @@ SHENFEN_OPTIONS = frozenset({'dashou', 'guanshi', 'zuzhang', 'shangjia'}) ROLE_CONFIG = { 'dashou': { - 'model': DashouRiTongji, - 'sort_field': 'chengjiao_zonge', - 'fields': ('jiedan_zongliang', 'chengjiao_zongliang', 'jiedan_zonge', 'chengjiao_zonge'), - 'int_fields': frozenset({'jiedan_zongliang', 'chengjiao_zongliang'}), + 'model': PlayerDailyStats, + 'id_field': 'PlayerID', + 'sort_field': 'CompletedAmount', + 'fields': ('AcceptedOrderTotal', 'CompletedOrderTotal', 'AcceptedAmount', 'CompletedAmount'), + 'int_fields': frozenset({'AcceptedOrderTotal', 'CompletedOrderTotal'}), }, 'guanshi': { - 'model': GuanshiRiTongji, - 'sort_field': 'shouru_zonge', - 'fields': ('yaoqing_dashou_shu', 'chongzhi_dashou_shu', 'shouru_zonge'), - 'int_fields': frozenset({'yaoqing_dashou_shu', 'chongzhi_dashou_shu'}), + 'model': ManagerDailyStats, + 'id_field': 'ManagerID', + 'sort_field': 'TotalIncome', + 'fields': ('InvitedPlayerCount', 'RechargedPlayerCount', 'TotalIncome'), + 'int_fields': frozenset({'InvitedPlayerCount', 'RechargedPlayerCount'}), }, 'zuzhang': { - 'model': ZuzhangRiTongji, - 'sort_field': 'shouru_zonge', - 'fields': ('yaoqing_guanshi_shu', 'fenyong_jine', 'shouru_zonge'), - 'int_fields': frozenset({'yaoqing_guanshi_shu'}), + 'model': LeaderDailyStats, + 'id_field': 'LeaderID', + 'sort_field': 'TotalIncome', + 'fields': ('InvitedManagerCount', 'CommissionAmount', 'TotalIncome'), + 'int_fields': frozenset({'InvitedManagerCount'}), }, 'shangjia': { - 'model': ShangjiaRiTongji, - 'sort_field': 'jiesuan_jine', - 'fields': ('paifa_dingdan_shu', 'paifa_jine', 'jiesuan_dingdan_shu', 'jiesuan_jine'), - 'int_fields': frozenset({'paifa_dingdan_shu', 'jiesuan_dingdan_shu'}), + 'model': MerchantDailyStats, + 'id_field': 'MerchantID', + 'sort_field': 'SettledAmount', + 'fields': ('AssignedOrderCount', 'AssignedAmount', 'SettledOrderCount', 'SettledAmount'), + 'int_fields': frozenset({'AssignedOrderCount', 'SettledOrderCount'}), }, } @@ -137,10 +141,11 @@ class PhbHqsjView(APIView): nick_map, avatar_map = self._load_user_display(rows, shenfen) cfg = ROLE_CONFIG[shenfen] int_fields = cfg['int_fields'] + id_field = cfg['id_field'] result_list = [] for idx, row in enumerate(rows): - yonghuid = row['yonghuid'] + yonghuid = row[id_field] item = { 'mingci': idx + 1, 'yonghuid': yonghuid, @@ -169,35 +174,37 @@ class PhbHqsjView(APIView): model = cfg['model'] sort_field = cfg['sort_field'] fields = cfg['fields'] + id_field = cfg['id_field'] annotate_kwargs = {f: Sum(f) for f in fields} if all_time: rows = ( model.objects - .values('yonghuid') + .values(id_field) .annotate(**annotate_kwargs) .order_by(f'-{sort_field}')[:MAX_RANK] ) return list(rows) if start_date == end_date: - qs = model.objects.filter(riqi=start_date).order_by(f'-{sort_field}')[:MAX_RANK] + qs = model.objects.filter(Date=start_date).order_by(f'-{sort_field}')[:MAX_RANK] return [ - {'yonghuid': r.yonghuid, **{f: getattr(r, f) for f in fields}} + {id_field: getattr(r, id_field), **{f: getattr(r, f) for f in fields}} for r in qs ] rows = ( model.objects - .filter(riqi__gte=start_date, riqi__lte=end_date) - .values('yonghuid') + .filter(Date__gte=start_date, Date__lte=end_date) + .values(id_field) .annotate(**annotate_kwargs) .order_by(f'-{sort_field}')[:MAX_RANK] ) return list(rows) def _load_user_display(self, rows, shenfen): - yonghuids = [r['yonghuid'] for r in rows if r.get('yonghuid')] + id_field = ROLE_CONFIG[shenfen]['id_field'] + yonghuids = [r[id_field] for r in rows if r.get(id_field)] if not yonghuids: return {}, {} diff --git a/users/tixian_shenhe_services.py b/users/tixian_shenhe_services.py index cfde88b..168806e 100644 --- a/users/tixian_shenhe_services.py +++ b/users/tixian_shenhe_services.py @@ -31,7 +31,7 @@ from config.models import Szjilu, TixianQuotaDefault from utils.fadan_utils import check_fadan_qiangdan_eligible from products.models import Huiyuangoumai -from backend.models import TixianRiTongji +from backend.models import WithdrawalDailyStats from utils.wechat_v3 import build_authorization @@ -445,12 +445,12 @@ def _get_active_auto_record(shenhe_danhao): def _get_platform_daily_total(leixing, today=None): - """平台今日已提现总额:只读 houtai.TixianRiTongji(到账金额口径)""" + """平台今日已提现总额:只读 houtai.WithdrawalDailyStats(到账金额口径)""" if today is None: today = date.today() try: - return TixianRiTongji.query.get(riqi=today, leixing=leixing).total_amount - except TixianRiTongji.DoesNotExist: + return WithdrawalDailyStats.query.get(Date=today, WithdrawalType=leixing).total_amount + except WithdrawalDailyStats.DoesNotExist: return decimal.Decimal('0.00') @@ -576,7 +576,7 @@ def check_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_d def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe_danhao=''): """ 收款前原子预占每日限额(必须在 transaction.atomic 内调用)。 - 锁定 TixianRiTongji + 用户行,校验通过后立即写入统计,防止高并发超额。 + 锁定 WithdrawalDailyStats + 用户行,校验通过后立即写入统计,防止高并发超额。 本审核单已有进行中/成功打款记录则视为已预占,不重复加计。 返回 (True, '', '') 或 (False, msg, 'personal'|'platform') """ @@ -617,9 +617,9 @@ def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe 'personal', ) - platform_stat, _ = TixianRiTongji.objects.select_for_update().get_or_create( - riqi=today, - leixing=leixing, + platform_stat, _ = WithdrawalDailyStats.objects.select_for_update().get_or_create( + Date=today, + WithdrawalType=leixing, defaults={'total_amount': decimal.Decimal('0.00')}, ) platform_limit = _get_platform_daily_limit(leixing) @@ -678,15 +678,15 @@ def release_collect_quota_reservation(user_main, leixing, jine, shijidaozhang): zuzhang.save(update_fields=['jinri_tixian']) try: - stat = TixianRiTongji.objects.select_for_update().get( - riqi=today, leixing=leixing, + stat = WithdrawalDailyStats.objects.select_for_update().get( + Date=today, WithdrawalType=leixing, ) stat.total_amount = max( stat.total_amount - shijidaozhang, decimal.Decimal('0.00'), ) stat.save(update_fields=['total_amount']) - except TixianRiTongji.DoesNotExist: + except WithdrawalDailyStats.DoesNotExist: pass except Exception as e: logger.error( @@ -741,7 +741,7 @@ def _quota_already_reserved_for_record(auto_record): def apply_transfer_success_quota(auto_record): """ 打款成功时累加每日限额统计(仅自动审核流程 shenhe_danhao 非空)。 - - houtai.TixianRiTongji.total_amount += 实际到账 + - houtai.WithdrawalDailyStats.total_amount += 实际到账 - 用户扩展表个人今日已提 += 申请金额(打手/管事/组长) 须在 transaction.atomic 内、且打款记录 zhuangtai 由 0→1 时调用一次。 """ @@ -758,9 +758,9 @@ def apply_transfer_success_quota(auto_record): logger.error('打款成功累加限额失败:用户不存在 %s', auto_record.yonghuid) return - platform_stat, _ = TixianRiTongji.objects.select_for_update().get_or_create( - riqi=today, - leixing=leixing, + platform_stat, _ = WithdrawalDailyStats.objects.select_for_update().get_or_create( + Date=today, + WithdrawalType=leixing, defaults={'total_amount': decimal.Decimal('0.00')}, ) platform_stat.total_amount += shijidaozhang diff --git a/users/views.py b/users/views.py index 53a50bc..238c977 100644 --- a/users/views.py +++ b/users/views.py @@ -84,7 +84,7 @@ from products.models import ( ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu ) from config.models import Qunpeizhi, Szjilu -from backend.models import ShangjiaRiTongji +from backend.models import MerchantDailyStats from rank.models import ( KaohePayTemp, Chenghao, ShenheJilu, KaoheCishuFeiyong, YonghuChenghao @@ -1088,13 +1088,13 @@ class ShangJiaXinXiView(APIView): }) response_data['chenghao_list'] = chenghao_list - # ------------------- 3. 从 ShangjiaRiTongji 获取今日统计 ------------------- + # ------------------- 3. 从 MerchantDailyStats 获取今日统计 ------------------- try: - today_stat = ShangjiaRiTongji.query.get(yonghuid=yonghuid, riqi=today) - jinri_paifa_dingdan = today_stat.paifa_dingdan_shu - jinri_paifa_jine = float(today_stat.paifa_jine) - jinri_tuikuan_dingdan = today_stat.tuikuan_dingdan_shu - except ShangjiaRiTongji.DoesNotExist: + today_stat = MerchantDailyStats.query.get(MerchantID=yonghuid, Date=today) + jinri_paifa_dingdan = today_stat.AssignedOrderCount + jinri_paifa_jine = float(today_stat.AssignedAmount) + jinri_tuikuan_dingdan = today_stat.RefundOrderCount + except MerchantDailyStats.DoesNotExist: jinri_paifa_dingdan = 0 jinri_paifa_jine = 0.00 jinri_tuikuan_dingdan = 0 @@ -1103,14 +1103,14 @@ class ShangJiaXinXiView(APIView): response_data['jinrituikuan'] = jinri_tuikuan_dingdan # 今日退款数(新增) response_data['riliushui'] = jinri_paifa_jine # 今日流水(派发总金额) - # ------------------- 4. 从 ShangjiaRiTongji 获取本月统计 ------------------- - month_stats = ShangjiaRiTongji.query.filter( - yonghuid=yonghuid, - riqi__gte=this_month_start, - riqi__lte=today + # ------------------- 4. 从 MerchantDailyStats 获取本月统计 ------------------- + month_stats = MerchantDailyStats.query.filter( + MerchantID=yonghuid, + Date__gte=this_month_start, + Date__lte=today ).aggregate( - total_paifa_jine=Sum('paifa_jine'), - total_paifa_dingdan=Sum('paifa_dingdan_shu') + total_paifa_jine=Sum('AssignedAmount'), + total_paifa_dingdan=Sum('AssignedOrderCount') ) yue_paifa_jine = float(month_stats.get('total_paifa_jine') or 0.00) # yue_paifa_dingdan = month_stats.get('total_paifa_dingdan') or 0 # 如果前端不需要可不加