Compare commits
252 Commits
2
...
c5ae30507b
| Author | SHA1 | Date | |
|---|---|---|---|
| c5ae30507b | |||
| 9d98ac9e83 | |||
| 7608faf396 | |||
| 63cb8e9ee9 | |||
|
|
0afeff8227 | ||
|
|
432387982e | ||
|
|
c7adec20db | ||
|
|
725a1386c3 | ||
|
|
0fc9c85503 | ||
|
|
593ed1ab09 | ||
|
|
2fb1411b78 | ||
|
|
d8ac8521d4 | ||
|
|
2dcd5cc9a6 | ||
|
|
0c40b326ea | ||
|
|
5d20d1a25e | ||
|
|
c21168ad2d | ||
|
|
9fe03727c5 | ||
|
|
9f4d57a677 | ||
|
|
9b636f1711 | ||
|
|
7f7859c2a4 | ||
|
|
3394fef79b | ||
|
|
2504997c53 | ||
|
|
5ef4ff7c91 | ||
|
|
cb03898986 | ||
|
|
e533424de5 | ||
|
|
b2fcff303b | ||
|
|
c7270bcff7 | ||
|
|
3452b0fb69 | ||
|
|
94fd40618e | ||
|
|
634796b012 | ||
|
|
31247b7915 | ||
|
|
0de3c164fb | ||
|
|
645b075f89 | ||
|
|
fb495197b8 | ||
|
|
dc71c4f1cb | ||
|
|
dd9e4afa49 | ||
|
|
42cb117a8a | ||
|
|
c336a893a0 | ||
|
|
188792328e | ||
|
|
218001399c | ||
|
|
de91089743 | ||
|
|
a45bc150e8 | ||
|
|
d7c09e230f | ||
|
|
3d2fb61df5 | ||
|
|
b152f95ef8 | ||
|
|
22f1781ea2 | ||
|
|
872dbedb6e | ||
|
|
5c7bb82a7b | ||
|
|
48bb43d9a8 | ||
|
|
7c9db3f360 | ||
|
|
b050a6895b | ||
|
|
2003ee01a5 | ||
|
|
5be696db9d | ||
|
|
3a1aaa9b61 | ||
|
|
54f8e23488 | ||
|
|
65477592af | ||
|
|
a6d32f848e | ||
|
|
ee7867b36f | ||
|
|
374dd26855 | ||
|
|
d19b6250ae | ||
|
|
e2e568bcda | ||
|
|
318e3e3216 | ||
|
|
93bf2193d2 | ||
|
|
fdecf2fe3c | ||
|
|
2445b39f3b | ||
|
|
8f21491c72 | ||
|
|
c8b67a2fc8 | ||
|
|
3083096cd7 | ||
|
|
cf0882575e | ||
|
|
fc63836cc7 | ||
|
|
992f4f4f18 | ||
|
|
aaf14264d8 | ||
|
|
0b202722e6 | ||
|
|
1aa8c2dc87 | ||
|
|
85531da9eb | ||
|
|
34e79aa753 | ||
|
|
65c5e6572e | ||
|
|
ff5b13e717 | ||
|
|
2b9ea6ccec | ||
|
|
41fa98e8b7 | ||
|
|
8884159edc | ||
|
|
1e5de2061a | ||
|
|
095a8e89e4 | ||
|
|
24949b1cb2 | ||
|
|
141f17859a | ||
|
|
01d73c5fae | ||
|
|
78c5442381 | ||
|
|
550061435e | ||
|
|
5ebcb56b65 | ||
|
|
af2578573c | ||
|
|
f6b99f96a4 | ||
|
|
d25c2dd862 | ||
|
|
a7e51aac3f | ||
|
|
f38f1216ea | ||
|
|
eb1fc6d2ae | ||
|
|
2f0838e552 | ||
|
|
deb1688d0c | ||
|
|
93e68fe20f | ||
|
|
815f344d19 | ||
|
|
a69c008f96 | ||
|
|
8ee7814a45 | ||
|
|
2bd45f8654 | ||
|
|
59f5a695d8 | ||
|
|
9575edcfc4 | ||
|
|
d1dc65e13b | ||
|
|
ff1e1a73dd | ||
|
|
7a2723fef7 | ||
|
|
0c4b28ea45 | ||
|
|
cca53bbc96 | ||
|
|
4714e2259b | ||
|
|
d56c85bca9 | ||
|
|
49482830f9 | ||
|
|
77e1d3f293 | ||
|
|
65613af195 | ||
|
|
8154b2639d | ||
|
|
7f2f0caaa1 | ||
|
|
8e5db24bad | ||
|
|
79f2d4f020 | ||
|
|
8c271f639a | ||
|
|
b1fbc3cd8f | ||
|
|
84665e0c5d | ||
|
|
3b7c539a56 | ||
|
|
600188a99a | ||
|
|
a6068f7e2b | ||
|
|
0827da594c | ||
|
|
a693e35177 | ||
|
|
08fc8f50f3 | ||
|
|
8e5662c56f | ||
|
|
45123d5b6f | ||
|
|
9cd7b6920a | ||
|
|
83519ac67b | ||
|
|
04c54d0733 | ||
|
|
0ac56c5b92 | ||
|
|
48bf50c340 | ||
|
|
467bb71736 | ||
|
|
bfc38c2d43 | ||
|
|
5ae01a9ff2 | ||
|
|
b33bd3eb18 | ||
|
|
c42f979082 | ||
|
|
32486a6e77 | ||
|
|
5c4663c8ec | ||
|
|
a8b6e46f61 | ||
|
|
574fe5e860 | ||
|
|
dcc4936428 | ||
|
|
ce9b09f096 | ||
|
|
98daa93abd | ||
|
|
8dae04c32c | ||
|
|
3f4712bb44 | ||
|
|
6e37477e4f | ||
|
|
bdebbb9348 | ||
|
|
45af7e1649 | ||
|
|
a07bd3d388 | ||
|
|
1ef7c62ab4 | ||
|
|
a1d5a2c3a3 | ||
|
|
24cd2ccea3 | ||
|
|
d19bcb844a | ||
|
|
86b4641558 | ||
|
|
da4bb47ee5 | ||
|
|
4dd3c1bbf0 | ||
|
|
2ce2aad956 | ||
|
|
4f6c06a488 | ||
|
|
7890d0992a | ||
|
|
a30bd69bf2 | ||
|
|
42af45f92c | ||
|
|
d68a1da2c2 | ||
|
|
b2520726dc | ||
|
|
c70b2ee7be | ||
|
|
993c0c54b0 | ||
|
|
4b704d48fa | ||
|
|
470546ae82 | ||
|
|
1d4d054c75 | ||
|
|
92704a86ca | ||
|
|
1ac05494d6 | ||
|
|
6fa0e1638d | ||
|
|
89632228cc | ||
|
|
7dc3064a88 | ||
|
|
0cf5341d73 | ||
|
|
e233ee4bea | ||
|
|
3fecb9390f | ||
|
|
4ded4ea50a | ||
|
|
157ac05632 | ||
|
|
1ce023552c | ||
|
|
e656760441 | ||
|
|
f851dbdefd | ||
|
|
a055131ec7 | ||
| f890369684 | |||
| a7f43a4e74 | |||
| a06202d297 | |||
| 0b3beca147 | |||
| f212e7203e | |||
| 212840d4ab | |||
| 52bd2dfd85 | |||
| c1118fc335 | |||
| 54ee08b3c5 | |||
| ef922b2b90 | |||
| 479fd8bbd8 | |||
| 7f5e76551c | |||
| 33eda43664 | |||
| e64697b64a | |||
| 90e0bde7f1 | |||
| 1a8348cf4b | |||
| ee42d80d93 | |||
| fcd2a5e8a9 | |||
| 8aa36e3764 | |||
| 0ff82d1bac | |||
| 15381c3051 | |||
| e717fc1763 | |||
| df927e141d | |||
| a699037826 | |||
| c3819ccd65 | |||
| 0cd20850e0 | |||
| 4963b06e21 | |||
| ce0fb79b39 | |||
| 46e5a5eeee | |||
| 67872f823c | |||
| c9bc811bc1 | |||
| 7df8a1a1d2 | |||
| 6f6af7553a | |||
| 854767a2c2 | |||
| d71a81f279 | |||
| 5b9436b994 | |||
| d3903c913b | |||
| 38fcfd730c | |||
| e672ac8d3f | |||
| b7f987f2c8 | |||
| f169b6569a | |||
| d93d3b3c6d | |||
| 05498904cf | |||
| 5c8b82df15 | |||
| d769dec2ec | |||
| 5ac9c785f9 | |||
| bb73a492cc | |||
| 5ebded322f | |||
| c093fa46c1 | |||
| c368b6cb73 | |||
| 53049d1588 | |||
|
|
e67bc17fa2 | ||
|
|
16845d0ce2 | ||
|
|
177d082d80 | ||
|
|
d96381fd4e | ||
| 854c832c07 | |||
| 54cc658f0b | |||
| f80ce51859 | |||
| 7ce545fedb | |||
| f9da920e6b | |||
| ca3a109c41 | |||
| 4aca437d2b | |||
| d00f0d08e5 | |||
| 72770ad73b | |||
| 61ddf81fb0 | |||
| 004012b59f | |||
| 461643079b |
@@ -1,2 +0,0 @@
|
|||||||
# 3. 在项目的 `__init__.py` 文件(与manage.py同级)中,添加两行代码
|
|
||||||
# 文件路径:a_long_dianjing/__init__.py
|
|
||||||
@@ -14,6 +14,8 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
|||||||
|
|
||||||
SECRET_KEY = app_secrets.SECRET_KEY
|
SECRET_KEY = app_secrets.SECRET_KEY
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
# 详细错误日志开关:True=在 except 块中输出完整堆栈到日志;False=仅记录错误摘要
|
||||||
|
DEBUG_DETAIL_LOG = True
|
||||||
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'www.abas.asia']
|
ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'www.abas.asia']
|
||||||
|
|
||||||
# ==================== 应用配置 ====================
|
# ==================== 应用配置 ====================
|
||||||
@@ -38,10 +40,15 @@ INSTALLED_APPS = [
|
|||||||
'utils',
|
'utils',
|
||||||
'shop',
|
'shop',
|
||||||
'rank',
|
'rank',
|
||||||
|
'merchant_ops',
|
||||||
|
'jituan',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
'utils.ip_filter_middleware.IPFilterMiddleware',
|
||||||
|
'utils.debug_middleware.DebugExceptionMiddleware',
|
||||||
'corsheaders.middleware.CorsMiddleware',
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
|
'jituan.middleware.ClubContextMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
@@ -51,6 +58,45 @@ MIDDLEWARE = [
|
|||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 日志配置 — 500 错误堆栈输出到 console,受 DEBUG_DETAIL_LOG 开关控制
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'verbose': {
|
||||||
|
'format': '[%(asctime)s] %(levelname)s %(name)s: %(message)s'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'verbose',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'django': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'INFO',
|
||||||
|
},
|
||||||
|
# 业务模块日志 — DEBUG_DETAIL_LOG=True 时输出 DEBUG 级别(含完整堆栈)
|
||||||
|
'users': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'DEBUG' if DEBUG_DETAIL_LOG else 'INFO',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'orders': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'DEBUG' if DEBUG_DETAIL_LOG else 'INFO',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
'utils.debug_middleware': {
|
||||||
|
'handlers': ['console'],
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
ROOT_URLCONF = 'a_long_dianjing.urls'
|
ROOT_URLCONF = 'a_long_dianjing.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
@@ -119,8 +165,8 @@ SIMPLE_JWT = {
|
|||||||
'LEEWAY': 0,
|
'LEEWAY': 0,
|
||||||
'AUTH_HEADER_TYPES': ('Bearer',),
|
'AUTH_HEADER_TYPES': ('Bearer',),
|
||||||
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
|
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
|
||||||
'USER_ID_FIELD': 'yonghuid',
|
'USER_ID_FIELD': 'UserUID',
|
||||||
'USER_ID_CLAIM': 'yonghuid',
|
'USER_ID_CLAIM': 'UserUID',
|
||||||
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
|
'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',
|
||||||
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
|
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
|
||||||
'TOKEN_TYPE_CLAIM': 'token_type',
|
'TOKEN_TYPE_CLAIM': 'token_type',
|
||||||
@@ -132,12 +178,13 @@ AUTHENTICATION_BACKENDS = [
|
|||||||
'django.contrib.auth.backends.ModelBackend',
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
]
|
]
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'users.UserMain'
|
AUTH_USER_MODEL = 'users.User'
|
||||||
|
|
||||||
# ==================== DRF 配置 ====================
|
# ==================== DRF 配置 ====================
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
'gvsdsdk.auth.jwt_auth.CompatibleJWTAuthentication',
|
||||||
|
'gvsdsdk.auth.elt_auth.ELTAuthentication',
|
||||||
),
|
),
|
||||||
'DEFAULT_PERMISSION_CLASSES': (
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
'rest_framework.permissions.IsAuthenticated',
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
@@ -177,8 +224,28 @@ CORS_ALLOW_METHODS = ("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
|
|||||||
CORS_ALLOW_HEADERS = [
|
CORS_ALLOW_HEADERS = [
|
||||||
"accept", "accept-encoding", "authorization", "content-type",
|
"accept", "accept-encoding", "authorization", "content-type",
|
||||||
"dnt", "origin", "user-agent", "x-csrftoken", "x-requested-with",
|
"dnt", "origin", "user-agent", "x-csrftoken", "x-requested-with",
|
||||||
|
"x-club-id", "x-club-scope",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# ==================== IP 访问控制 ====================
|
||||||
|
# 访问模式:
|
||||||
|
# production — 生产模式(默认):放行所有 IP
|
||||||
|
# whitelist — 白名单模式:仅允许白名单内 IP 访问,其余返回 403
|
||||||
|
# blacklist — 黑名单模式:禁止黑名单内 IP 访问,其余放行
|
||||||
|
IP_ACCESS_MODE = 'production'
|
||||||
|
# 白名单(whitelist 模式下生效)— 支持单 IP 和 CIDR 网段
|
||||||
|
IP_WHITELIST = [
|
||||||
|
'111.32.64.171',
|
||||||
|
'43.142.0.208',
|
||||||
|
'223.104.7.77',
|
||||||
|
'112.7.200.17'
|
||||||
|
]
|
||||||
|
# 黑名单(blacklist 模式下生效)— 支持单 IP 和 CIDR 网段
|
||||||
|
IP_BLACKLIST = []
|
||||||
|
|
||||||
|
# 免过滤路径(任何模式下都放行,如管理后台、健康检查)
|
||||||
|
IP_FILTER_EXEMPT_PATHS = []
|
||||||
|
|
||||||
# ==================== 腾讯云 COS ====================
|
# ==================== 腾讯云 COS ====================
|
||||||
COS_SECRET_ID = app_secrets.COS_SECRET_ID
|
COS_SECRET_ID = app_secrets.COS_SECRET_ID
|
||||||
COS_SECRET_KEY = app_secrets.COS_SECRET_KEY
|
COS_SECRET_KEY = app_secrets.COS_SECRET_KEY
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ urlpatterns = [
|
|||||||
path('houtai/', include('backend.urls')),
|
path('houtai/', include('backend.urls')),
|
||||||
path('shangdian/', include('shop.urls')),
|
path('shangdian/', include('shop.urls')),
|
||||||
path('dengji/', include('rank.urls')),
|
path('dengji/', include('rank.urls')),
|
||||||
|
path('jituan/', include('jituan.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
# 重要说明:
|
# 重要说明:
|
||||||
|
|||||||
@@ -1,296 +1,303 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-04-06 23:31
|
# Generated by Django 4.2.27 on 2026-06-18 03:51
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
]
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
operations = [
|
||||||
name="AbnormalUserLog",
|
migrations.CreateModel(
|
||||||
fields=[
|
name='AbnormalUserLog',
|
||||||
(
|
fields=[
|
||||||
"id",
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
models.BigAutoField(
|
('account_id', models.CharField(db_index=True, max_length=20, verbose_name='账号ID(phone)')),
|
||||||
auto_created=True,
|
('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='IP地址')),
|
||||||
primary_key=True,
|
('real_ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='真实IP(穿透代理后)')),
|
||||||
serialize=False,
|
('x_forwarded_for', models.CharField(blank=True, default='', max_length=255, verbose_name='X-Forwarded-For')),
|
||||||
verbose_name="ID",
|
('user_agent', models.CharField(blank=True, default='', max_length=500, verbose_name='User-Agent')),
|
||||||
),
|
('request_method', models.CharField(blank=True, default='', max_length=10, verbose_name='请求方法')),
|
||||||
),
|
('request_path', models.CharField(blank=True, default='', max_length=500, verbose_name='请求路径')),
|
||||||
(
|
('request_params', models.TextField(blank=True, default='', verbose_name='请求参数(GET/POST)')),
|
||||||
"account_id",
|
('request_body', models.TextField(blank=True, default='', verbose_name='请求体(原始)')),
|
||||||
models.CharField(
|
('request_headers', models.TextField(blank=True, default='', verbose_name='请求头(JSON格式)')),
|
||||||
db_index=True, max_length=20, verbose_name="账号ID(phone)"
|
('attempt_type', models.CharField(blank=True, default='', max_length=50, verbose_name='攻击类型')),
|
||||||
),
|
('detail', models.TextField(blank=True, default='', verbose_name='详细描述')),
|
||||||
),
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
(
|
],
|
||||||
"ip_address",
|
options={
|
||||||
models.GenericIPAddressField(
|
'verbose_name': '异常用户日志',
|
||||||
blank=True, null=True, verbose_name="IP地址"
|
'verbose_name_plural': '异常用户日志',
|
||||||
),
|
'db_table': 'abnormal_user_log',
|
||||||
),
|
},
|
||||||
(
|
),
|
||||||
"real_ip",
|
migrations.CreateModel(
|
||||||
models.GenericIPAddressField(
|
name='LeaderDailyStats',
|
||||||
blank=True, null=True, verbose_name="真实IP(穿透代理后)"
|
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='日期')),
|
||||||
"x_forwarded_for",
|
('InvitedManagerCount', models.IntegerField(db_column='yaoqing_guanshi_shu', default=0, verbose_name='邀请管事总数')),
|
||||||
models.CharField(
|
('TotalIncome', models.DecimalField(db_column='shouru_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='收入总额(分佣)')),
|
||||||
blank=True,
|
('CommissionAmount', models.DecimalField(db_column='fenyong_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='分佣金额')),
|
||||||
default="",
|
],
|
||||||
max_length=255,
|
options={
|
||||||
verbose_name="X-Forwarded-For",
|
'verbose_name': '组长每日统计',
|
||||||
),
|
'db_table': 'zuzhang_ri_tongji',
|
||||||
),
|
},
|
||||||
(
|
),
|
||||||
"user_agent",
|
migrations.CreateModel(
|
||||||
models.CharField(
|
name='ManagerDailyStats',
|
||||||
blank=True,
|
fields=[
|
||||||
default="",
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
max_length=500,
|
('ManagerID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='管事用户ID')),
|
||||||
verbose_name="User-Agent",
|
('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='收入总额')),
|
||||||
"request_method",
|
],
|
||||||
models.CharField(
|
options={
|
||||||
blank=True, default="", max_length=10, verbose_name="请求方法"
|
'verbose_name': '管事每日统计',
|
||||||
),
|
'db_table': 'guanshi_ri_tongji',
|
||||||
),
|
},
|
||||||
(
|
),
|
||||||
"request_path",
|
migrations.CreateModel(
|
||||||
models.CharField(
|
name='ManagerRenewalDailyStats',
|
||||||
blank=True, default="", max_length=500, verbose_name="请求路径"
|
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='日期')),
|
||||||
"request_params",
|
('RenewalTotal', models.IntegerField(db_column='xufei_zongshu', default=0, verbose_name='今日续费总数')),
|
||||||
models.TextField(
|
('RenewalRevenue', models.DecimalField(db_column='xufei_shouyi', decimal_places=2, default=0.0, max_digits=12, verbose_name='续费收益金额')),
|
||||||
blank=True, default="", verbose_name="请求参数(GET/POST)"
|
],
|
||||||
),
|
options={
|
||||||
),
|
'verbose_name': '管事续费每日统计',
|
||||||
(
|
'db_table': 'guanshi_xufei_ri_tongji',
|
||||||
"request_body",
|
},
|
||||||
models.TextField(
|
),
|
||||||
blank=True, default="", verbose_name="请求体(原始)"
|
migrations.CreateModel(
|
||||||
),
|
name='MerchantDailyStats',
|
||||||
),
|
fields=[
|
||||||
(
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
"request_headers",
|
('MerchantID', models.CharField(db_column='yonghuid', db_index=True, max_length=7, verbose_name='商家用户ID')),
|
||||||
models.TextField(
|
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
|
||||||
blank=True, default="", verbose_name="请求头(JSON格式)"
|
('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='结算总金额')),
|
||||||
"attempt_type",
|
('RefundOrderCount', models.IntegerField(db_column='tuikuan_dingdan_shu', default=0, verbose_name='退款订单总数')),
|
||||||
models.CharField(
|
('RefundAmount', models.DecimalField(db_column='tuikuan_jine', decimal_places=2, default=0.0, max_digits=12, verbose_name='退款总金额')),
|
||||||
blank=True, default="", max_length=50, verbose_name="攻击类型"
|
],
|
||||||
),
|
options={
|
||||||
),
|
'verbose_name': '商家每日统计',
|
||||||
(
|
'db_table': 'shangjia_ri_tongji',
|
||||||
"detail",
|
},
|
||||||
models.TextField(blank=True, default="", verbose_name="详细描述"),
|
),
|
||||||
),
|
migrations.CreateModel(
|
||||||
(
|
name='Permission',
|
||||||
"create_time",
|
fields=[
|
||||||
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
|
('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='权限名称')),
|
||||||
options={
|
('description', models.CharField(blank=True, default='', max_length=200, verbose_name='权限描述')),
|
||||||
"verbose_name": "异常用户日志",
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
"verbose_name_plural": "异常用户日志",
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
"db_table": "abnormal_user_log",
|
],
|
||||||
},
|
options={
|
||||||
),
|
'verbose_name': '权限',
|
||||||
migrations.CreateModel(
|
'verbose_name_plural': '权限',
|
||||||
name="Permission",
|
'db_table': 'permission',
|
||||||
fields=[
|
'managed': True,
|
||||||
(
|
},
|
||||||
"perm_id",
|
),
|
||||||
models.AutoField(
|
migrations.CreateModel(
|
||||||
primary_key=True, serialize=False, verbose_name="权限ID"
|
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')),
|
||||||
"perm_code",
|
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
|
||||||
models.CharField(
|
('AcceptedOrderTotal', models.IntegerField(db_column='jiedan_zongliang', default=0, verbose_name='接单总量')),
|
||||||
db_index=True,
|
('CompletedOrderTotal', models.IntegerField(db_column='chengjiao_zongliang', default=0, verbose_name='成交单总量')),
|
||||||
max_length=100,
|
('AcceptedAmount', models.DecimalField(db_column='jiedan_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='接单总金额')),
|
||||||
unique=True,
|
('CompletedAmount', models.DecimalField(db_column='chengjiao_zonge', decimal_places=2, default=0.0, max_digits=12, verbose_name='成交总金额')),
|
||||||
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={
|
||||||
"perm_name",
|
'verbose_name': '打手每日统计',
|
||||||
models.CharField(max_length=100, verbose_name="权限名称"),
|
'db_table': 'dashou_ri_tongji',
|
||||||
),
|
},
|
||||||
(
|
),
|
||||||
"description",
|
migrations.CreateModel(
|
||||||
models.CharField(
|
name='Role',
|
||||||
blank=True, default="", max_length=200, verbose_name="权限描述"
|
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='角色名称')),
|
||||||
"create_time",
|
('description', models.CharField(blank=True, default='', max_length=200, verbose_name='角色描述')),
|
||||||
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
),
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
(
|
],
|
||||||
"update_time",
|
options={
|
||||||
models.DateTimeField(auto_now=True, verbose_name="更新时间"),
|
'verbose_name': '角色',
|
||||||
),
|
'verbose_name_plural': '角色',
|
||||||
],
|
'db_table': 'role',
|
||||||
options={
|
'managed': True,
|
||||||
"verbose_name": "权限",
|
},
|
||||||
"verbose_name_plural": "权限",
|
),
|
||||||
"db_table": "permission",
|
migrations.CreateModel(
|
||||||
},
|
name='WithdrawalDailyStats',
|
||||||
),
|
fields=[
|
||||||
migrations.CreateModel(
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
name="Role",
|
('Date', models.DateField(db_column='riqi', db_index=True, verbose_name='日期')),
|
||||||
fields=[
|
('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='当天提现总额(元)')),
|
||||||
"role_id",
|
],
|
||||||
models.AutoField(
|
options={
|
||||||
primary_key=True, serialize=False, verbose_name="角色ID"
|
'verbose_name': '每日提现统计',
|
||||||
),
|
'db_table': 'tixian_ri_tongji',
|
||||||
),
|
'indexes': [models.Index(fields=['Date', 'WithdrawalType'], name='tixian_ri_t_riqi_c18bb2_idx')],
|
||||||
(
|
'unique_together': {('Date', 'WithdrawalType')},
|
||||||
"role_code",
|
},
|
||||||
models.CharField(
|
),
|
||||||
db_index=True,
|
migrations.CreateModel(
|
||||||
max_length=50,
|
name='UserRole',
|
||||||
unique=True,
|
fields=[
|
||||||
verbose_name="角色编码",
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
),
|
('account_id', models.CharField(db_index=True, max_length=11, verbose_name='账号ID')),
|
||||||
),
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
("role_name", models.CharField(max_length=50, verbose_name="角色名称")),
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
(
|
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色')),
|
||||||
"description",
|
],
|
||||||
models.CharField(
|
options={
|
||||||
blank=True, default="", max_length=200, verbose_name="角色描述"
|
'verbose_name': '用户角色关联',
|
||||||
),
|
'verbose_name_plural': '用户角色关联',
|
||||||
),
|
'db_table': 'user_role',
|
||||||
(
|
'managed': True,
|
||||||
"create_time",
|
},
|
||||||
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
|
),
|
||||||
),
|
migrations.CreateModel(
|
||||||
(
|
name='RolePermission',
|
||||||
"update_time",
|
fields=[
|
||||||
models.DateTimeField(auto_now=True, verbose_name="更新时间"),
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
),
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
],
|
('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.permission', verbose_name='权限')),
|
||||||
options={
|
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色')),
|
||||||
"verbose_name": "角色",
|
],
|
||||||
"verbose_name_plural": "角色",
|
options={
|
||||||
"db_table": "role",
|
'verbose_name': '角色权限关联',
|
||||||
},
|
'verbose_name_plural': '角色权限关联',
|
||||||
),
|
'db_table': 'role_permission',
|
||||||
migrations.CreateModel(
|
'managed': True,
|
||||||
name="UserRole",
|
},
|
||||||
fields=[
|
),
|
||||||
("id", models.AutoField(primary_key=True, serialize=False)),
|
migrations.AddIndex(
|
||||||
(
|
model_name='role',
|
||||||
"account_id",
|
index=models.Index(fields=['role_code'], name='role_role_co_b9f497_idx'),
|
||||||
models.CharField(
|
),
|
||||||
db_index=True, max_length=11, verbose_name="账号ID"
|
migrations.AddIndex(
|
||||||
),
|
model_name='playerdailystats',
|
||||||
),
|
index=models.Index(fields=['Date', '-AcceptedOrderTotal'], name='dashou_ri_t_riqi_95a785_idx'),
|
||||||
(
|
),
|
||||||
"create_time",
|
migrations.AddIndex(
|
||||||
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
|
model_name='playerdailystats',
|
||||||
),
|
index=models.Index(fields=['Date', '-CompletedOrderTotal'], name='dashou_ri_t_riqi_ad9c1a_idx'),
|
||||||
(
|
),
|
||||||
"update_time",
|
migrations.AddIndex(
|
||||||
models.DateTimeField(auto_now=True, verbose_name="更新时间"),
|
model_name='playerdailystats',
|
||||||
),
|
index=models.Index(fields=['Date', '-AcceptedAmount'], name='dashou_ri_t_riqi_5da703_idx'),
|
||||||
(
|
),
|
||||||
"role",
|
migrations.AddIndex(
|
||||||
models.ForeignKey(
|
model_name='playerdailystats',
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
index=models.Index(fields=['Date', '-CompletedAmount'], name='dashou_ri_t_riqi_0a3156_idx'),
|
||||||
to="backend.role",
|
),
|
||||||
verbose_name="角色",
|
migrations.AlterUniqueTogether(
|
||||||
),
|
name='playerdailystats',
|
||||||
),
|
unique_together={('PlayerID', 'Date')},
|
||||||
],
|
),
|
||||||
options={
|
migrations.AddIndex(
|
||||||
"verbose_name": "用户角色关联",
|
model_name='permission',
|
||||||
"verbose_name_plural": "用户角色关联",
|
index=models.Index(fields=['perm_code'], name='permission_perm_co_56cbe3_idx'),
|
||||||
"db_table": "user_role",
|
),
|
||||||
},
|
migrations.AddIndex(
|
||||||
),
|
model_name='merchantdailystats',
|
||||||
migrations.CreateModel(
|
index=models.Index(fields=['Date', '-AssignedAmount'], name='shangjia_ri_riqi_d00f7f_idx'),
|
||||||
name="RolePermission",
|
),
|
||||||
fields=[
|
migrations.AddIndex(
|
||||||
("id", models.AutoField(primary_key=True, serialize=False)),
|
model_name='merchantdailystats',
|
||||||
(
|
index=models.Index(fields=['Date', '-SettledAmount'], name='shangjia_ri_riqi_25772a_idx'),
|
||||||
"create_time",
|
),
|
||||||
models.DateTimeField(auto_now_add=True, verbose_name="创建时间"),
|
migrations.AddIndex(
|
||||||
),
|
model_name='merchantdailystats',
|
||||||
(
|
index=models.Index(fields=['Date', '-AssignedOrderCount'], name='shangjia_ri_riqi_e05c54_idx'),
|
||||||
"permission",
|
),
|
||||||
models.ForeignKey(
|
migrations.AlterUniqueTogether(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
name='merchantdailystats',
|
||||||
to="backend.permission",
|
unique_together={('MerchantID', 'Date')},
|
||||||
verbose_name="权限",
|
),
|
||||||
),
|
migrations.AddIndex(
|
||||||
),
|
model_name='managerrenewaldailystats',
|
||||||
(
|
index=models.Index(fields=['Date', '-RenewalTotal'], name='guanshi_xuf_riqi_7c128f_idx'),
|
||||||
"role",
|
),
|
||||||
models.ForeignKey(
|
migrations.AddIndex(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
model_name='managerrenewaldailystats',
|
||||||
to="backend.role",
|
index=models.Index(fields=['Date', '-RenewalRevenue'], name='guanshi_xuf_riqi_8f09b5_idx'),
|
||||||
verbose_name="角色",
|
),
|
||||||
),
|
migrations.AlterUniqueTogether(
|
||||||
),
|
name='managerrenewaldailystats',
|
||||||
],
|
unique_together={('ManagerID', 'Date')},
|
||||||
options={
|
),
|
||||||
"verbose_name": "角色权限关联",
|
migrations.AddIndex(
|
||||||
"verbose_name_plural": "角色权限关联",
|
model_name='managerdailystats',
|
||||||
"db_table": "role_permission",
|
index=models.Index(fields=['Date', '-InvitedPlayerCount'], name='guanshi_ri__riqi_075e1a_idx'),
|
||||||
},
|
),
|
||||||
),
|
migrations.AddIndex(
|
||||||
migrations.AddIndex(
|
model_name='managerdailystats',
|
||||||
model_name="role",
|
index=models.Index(fields=['Date', '-RechargedPlayerCount'], name='guanshi_ri__riqi_78a79d_idx'),
|
||||||
index=models.Index(fields=["role_code"], name="role_role_co_b9f497_idx"),
|
),
|
||||||
),
|
migrations.AddIndex(
|
||||||
migrations.AddIndex(
|
model_name='managerdailystats',
|
||||||
model_name="permission",
|
index=models.Index(fields=['Date', '-TotalIncome'], name='guanshi_ri__riqi_334f52_idx'),
|
||||||
index=models.Index(
|
),
|
||||||
fields=["perm_code"], name="permission_perm_co_56cbe3_idx"
|
migrations.AlterUniqueTogether(
|
||||||
),
|
name='managerdailystats',
|
||||||
),
|
unique_together={('ManagerID', 'Date')},
|
||||||
migrations.AddIndex(
|
),
|
||||||
model_name="abnormaluserlog",
|
migrations.AddIndex(
|
||||||
index=models.Index(
|
model_name='leaderdailystats',
|
||||||
fields=["account_id"], name="abnormal_us_account_425899_idx"
|
index=models.Index(fields=['Date', '-InvitedManagerCount'], name='zuzhang_ri__riqi_87169e_idx'),
|
||||||
),
|
),
|
||||||
),
|
migrations.AddIndex(
|
||||||
migrations.AddIndex(
|
model_name='leaderdailystats',
|
||||||
model_name="abnormaluserlog",
|
index=models.Index(fields=['Date', '-TotalIncome'], name='zuzhang_ri__riqi_1d7e51_idx'),
|
||||||
index=models.Index(
|
),
|
||||||
fields=["ip_address"], name="abnormal_us_ip_addr_3d612b_idx"
|
migrations.AlterUniqueTogether(
|
||||||
),
|
name='leaderdailystats',
|
||||||
),
|
unique_together={('LeaderID', 'Date')},
|
||||||
migrations.AddIndex(
|
),
|
||||||
model_name="abnormaluserlog",
|
migrations.AddIndex(
|
||||||
index=models.Index(
|
model_name='abnormaluserlog',
|
||||||
fields=["create_time"], name="abnormal_us_create__ee16da_idx"
|
index=models.Index(fields=['account_id'], name='abnormal_us_account_425899_idx'),
|
||||||
),
|
),
|
||||||
),
|
migrations.AddIndex(
|
||||||
migrations.AlterUniqueTogether(
|
model_name='abnormaluserlog',
|
||||||
name="userrole",
|
index=models.Index(fields=['ip_address'], name='abnormal_us_ip_addr_3d612b_idx'),
|
||||||
unique_together={("account_id", "role")},
|
),
|
||||||
),
|
migrations.AddIndex(
|
||||||
migrations.AlterUniqueTogether(
|
model_name='abnormaluserlog',
|
||||||
name="rolepermission",
|
index=models.Index(fields=['CreateTime'], name='abnormal_us_create__ee16da_idx'),
|
||||||
unique_together={("role", "permission")},
|
),
|
||||||
),
|
migrations.AlterUniqueTogether(
|
||||||
]
|
name='userrole',
|
||||||
|
unique_together={('account_id', 'role')},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='rolepermission',
|
||||||
|
unique_together={('role', 'permission')},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.27 on 2026-06-19 19:15
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('backend', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameIndex(
|
||||||
|
model_name='abnormaluserlog',
|
||||||
|
new_name='abnormal_us_CreateT_38f96c_idx',
|
||||||
|
old_name='abnormal_us_create__ee16da_idx',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,339 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-04-09 14:53
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("backend", "0001_initial"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
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="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")},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-04-10 18:05
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("backend", "0002_zuzhangritongji_shangjiaritongji_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
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, "组长")],
|
|
||||||
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")},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
40
backend/migrations/0003_withdrawaldailystats_club_id.py
Normal file
40
backend/migrations/0003_withdrawaldailystats_club_id.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.models import Max
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_withdrawal_daily(apps, schema_editor):
|
||||||
|
Model = apps.get_model('backend', 'WithdrawalDailyStats')
|
||||||
|
for row in Model.objects.values('club_id', 'Date', 'WithdrawalType').annotate(max_id=Max('id')):
|
||||||
|
Model.objects.filter(
|
||||||
|
club_id=row['club_id'],
|
||||||
|
Date=row['Date'],
|
||||||
|
WithdrawalType=row['WithdrawalType'],
|
||||||
|
).exclude(id=row['max_id']).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('backend', '0002_rename_abnormal_us_create__ee16da_idx_abnormal_us_createt_38f96c_idx'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='withdrawaldailystats',
|
||||||
|
name='club_id',
|
||||||
|
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='withdrawaldailystats',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.RunPython(_dedupe_withdrawal_daily, migrations.RunPython.noop),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='withdrawaldailystats',
|
||||||
|
unique_together={('club_id', 'Date', 'WithdrawalType')},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='withdrawaldailystats',
|
||||||
|
index=models.Index(fields=['club_id', 'Date', 'WithdrawalType'], name='tixian_ri_club_date_type_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-06-15 01:42
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('backend', '0003_tixianritongji'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='rolepermission',
|
|
||||||
name='permission',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.permission', verbose_name='权限'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='rolepermission',
|
|
||||||
name='role',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='tixianritongji',
|
|
||||||
name='leixing',
|
|
||||||
field=models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], verbose_name='用户类型'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='userrole',
|
|
||||||
name='role',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='backend.role', verbose_name='角色'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
91
backend/migrations/0004_role_daily_club_id.py
Normal file
91
backend/migrations/0004_role_daily_club_id.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.models import Max
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_role_daily(apps, schema_editor, model_name, user_field):
|
||||||
|
Model = apps.get_model('backend', model_name)
|
||||||
|
for row in Model.objects.values('club_id', user_field, 'Date').annotate(max_id=Max('id')):
|
||||||
|
Model.objects.filter(
|
||||||
|
club_id=row['club_id'],
|
||||||
|
**{user_field: row[user_field], 'Date': row['Date']},
|
||||||
|
).exclude(id=row['max_id']).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_merchant(apps, schema_editor):
|
||||||
|
_dedupe_role_daily(apps, schema_editor, 'MerchantDailyStats', 'MerchantID')
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_player(apps, schema_editor):
|
||||||
|
_dedupe_role_daily(apps, schema_editor, 'PlayerDailyStats', 'PlayerID')
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_leader(apps, schema_editor):
|
||||||
|
_dedupe_role_daily(apps, schema_editor, 'LeaderDailyStats', 'LeaderID')
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_manager(apps, schema_editor):
|
||||||
|
_dedupe_role_daily(apps, schema_editor, 'ManagerDailyStats', 'ManagerID')
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_manager_renewal(apps, schema_editor):
|
||||||
|
_dedupe_role_daily(apps, schema_editor, 'ManagerRenewalDailyStats', 'ManagerID')
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('backend', '0003_withdrawaldailystats_club_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='merchantdailystats',
|
||||||
|
name='club_id',
|
||||||
|
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='playerdailystats',
|
||||||
|
name='club_id',
|
||||||
|
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='leaderdailystats',
|
||||||
|
name='club_id',
|
||||||
|
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='managerdailystats',
|
||||||
|
name='club_id',
|
||||||
|
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='managerrenewaldailystats',
|
||||||
|
name='club_id',
|
||||||
|
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(_dedupe_merchant, migrations.RunPython.noop),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='merchantdailystats',
|
||||||
|
unique_together={('club_id', 'MerchantID', 'Date')},
|
||||||
|
),
|
||||||
|
migrations.RunPython(_dedupe_player, migrations.RunPython.noop),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='playerdailystats',
|
||||||
|
unique_together={('club_id', 'PlayerID', 'Date')},
|
||||||
|
),
|
||||||
|
migrations.RunPython(_dedupe_leader, migrations.RunPython.noop),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='leaderdailystats',
|
||||||
|
unique_together={('club_id', 'LeaderID', 'Date')},
|
||||||
|
),
|
||||||
|
migrations.RunPython(_dedupe_manager, migrations.RunPython.noop),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='managerdailystats',
|
||||||
|
unique_together={('club_id', 'ManagerID', 'Date')},
|
||||||
|
),
|
||||||
|
migrations.RunPython(_dedupe_manager_renewal, migrations.RunPython.noop),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='managerrenewaldailystats',
|
||||||
|
unique_together={('club_id', 'ManagerID', 'Date')},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,271 +1,333 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from gvsdsdk.model_base import QModel
|
from gvsdsdk.model_base import QModel
|
||||||
|
|
||||||
|
|
||||||
# ==================== 1. 角色表 ====================
|
# ==================== 1. 角色表 ====================
|
||||||
class Role(QModel):
|
class Role(QModel):
|
||||||
"""角色表"""
|
"""角色表"""
|
||||||
role_id = models.AutoField(primary_key=True, verbose_name='角色ID')
|
role_id = models.AutoField(primary_key=True, verbose_name='角色ID')
|
||||||
role_code = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='角色编码')
|
role_code = models.CharField(max_length=50, unique=True, db_index=True, verbose_name='角色编码')
|
||||||
role_name = models.CharField(max_length=50, verbose_name='角色名称')
|
role_name = models.CharField(max_length=50, verbose_name='角色名称')
|
||||||
description = models.CharField(max_length=200, blank=True, default='', verbose_name='角色描述')
|
description = models.CharField(max_length=200, blank=True, default='', verbose_name='角色描述')
|
||||||
|
|
||||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'role'
|
db_table = 'role'
|
||||||
verbose_name = '角色'
|
verbose_name = '角色'
|
||||||
verbose_name_plural = '角色'
|
verbose_name_plural = '角色'
|
||||||
indexes = [
|
managed = True
|
||||||
models.Index(fields=['role_code']),
|
indexes = [
|
||||||
]
|
models.Index(fields=['role_code']),
|
||||||
|
]
|
||||||
def __str__(self):
|
|
||||||
return self.role_name
|
def __str__(self):
|
||||||
|
return self.role_name
|
||||||
|
|
||||||
# ==================== 2. 权限表 ====================
|
|
||||||
class Permission(QModel):
|
# ==================== 2. 权限表 ====================
|
||||||
"""权限表"""
|
class Permission(QModel):
|
||||||
perm_id = models.AutoField(primary_key=True, verbose_name='权限ID')
|
"""权限表"""
|
||||||
perm_code = models.CharField(max_length=100, unique=True, db_index=True, verbose_name='权限编码')
|
perm_id = models.AutoField(primary_key=True, verbose_name='权限ID')
|
||||||
perm_name = models.CharField(max_length=100, verbose_name='权限名称')
|
perm_code = models.CharField(max_length=100, unique=True, db_index=True, verbose_name='权限编码')
|
||||||
description = models.CharField(max_length=200, blank=True, default='', verbose_name='权限描述')
|
perm_name = models.CharField(max_length=100, verbose_name='权限名称')
|
||||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
description = models.CharField(max_length=200, blank=True, default='', verbose_name='权限描述')
|
||||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||||
|
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||||
class Meta:
|
|
||||||
db_table = 'permission'
|
class Meta:
|
||||||
verbose_name = '权限'
|
db_table = 'permission'
|
||||||
verbose_name_plural = '权限'
|
verbose_name = '权限'
|
||||||
indexes = [
|
verbose_name_plural = '权限'
|
||||||
models.Index(fields=['perm_code']),
|
managed = True
|
||||||
]
|
indexes = [
|
||||||
|
models.Index(fields=['perm_code']),
|
||||||
def __str__(self):
|
]
|
||||||
return f"{self.perm_name} ({self.perm_code})"
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.perm_name} ({self.perm_code})"
|
||||||
# ==================== 3. 角色-权限关联表 ====================
|
|
||||||
class RolePermission(QModel):
|
|
||||||
"""角色权限关联表"""
|
# ==================== 3. 角色-权限关联表 ====================
|
||||||
id = models.AutoField(primary_key=True)
|
class RolePermission(QModel):
|
||||||
role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色')
|
"""角色权限关联表"""
|
||||||
permission = models.ForeignKey(Permission, on_delete=models.CASCADE, db_index=True, verbose_name='权限')
|
id = models.AutoField(primary_key=True)
|
||||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色')
|
||||||
|
permission = models.ForeignKey(Permission, on_delete=models.CASCADE, db_index=True, verbose_name='权限')
|
||||||
class Meta:
|
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||||
db_table = 'role_permission'
|
|
||||||
verbose_name = '角色权限关联'
|
class Meta:
|
||||||
verbose_name_plural = '角色权限关联'
|
db_table = 'role_permission'
|
||||||
unique_together = [['role', 'permission']] # 防止重复授权
|
verbose_name = '角色权限关联'
|
||||||
|
verbose_name_plural = '角色权限关联'
|
||||||
def __str__(self):
|
managed = True
|
||||||
return f"{self.role.role_name} -> {self.permission.perm_name}"
|
unique_together = [['role', 'permission']]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
# ==================== 4. 用户-角色关联表 ====================
|
return f"{self.role.role_name} -> {self.permission.perm_name}"
|
||||||
class UserRole(QModel):
|
|
||||||
"""用户角色关联表"""
|
|
||||||
id = models.AutoField(primary_key=True)
|
# ==================== 4. 用户-角色关联表 ====================
|
||||||
account_id = models.CharField(max_length=11, db_index=True, verbose_name='账号ID') # 对应 UserMain 的 phone 字段
|
class UserRole(QModel):
|
||||||
role = models.ForeignKey(Role, on_delete=models.CASCADE, db_index=True, verbose_name='角色')
|
"""用户角色关联表"""
|
||||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
id = models.AutoField(primary_key=True)
|
||||||
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
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='角色')
|
||||||
class Meta:
|
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||||
db_table = 'user_role'
|
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||||
verbose_name = '用户角色关联'
|
|
||||||
verbose_name_plural = '用户角色关联'
|
class Meta:
|
||||||
unique_together = [['account_id', 'role']] # 同一用户同一角色只能有一条
|
db_table = 'user_role'
|
||||||
|
verbose_name = '用户角色关联'
|
||||||
def __str__(self):
|
verbose_name_plural = '用户角色关联'
|
||||||
return f"{self.account_id} -> {self.role.role_name}"
|
managed = True
|
||||||
|
unique_together = [['account_id', 'role']]
|
||||||
|
|
||||||
class AbnormalUserLog(QModel):
|
def __str__(self):
|
||||||
"""记录恶意尝试越权操作的用户信息"""
|
return f"{self.account_id} -> {self.role.role_name}"
|
||||||
# 账号信息
|
|
||||||
account_id = models.CharField(max_length=20, db_index=True, verbose_name='账号ID(phone)')
|
|
||||||
|
# ==================== 5. 异常用户日志 ====================
|
||||||
# 网络信息
|
class AbnormalUserLog(QModel):
|
||||||
ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP地址')
|
"""记录恶意尝试越权操作的用户信息"""
|
||||||
real_ip = models.GenericIPAddressField(null=True, blank=True, verbose_name='真实IP(穿透代理后)')
|
account_id = models.CharField(max_length=20, db_index=True, verbose_name='账号ID(phone)')
|
||||||
x_forwarded_for = models.CharField(max_length=255, blank=True, default='', verbose_name='X-Forwarded-For')
|
ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name='IP地址')
|
||||||
user_agent = models.CharField(max_length=500, blank=True, default='', verbose_name='User-Agent')
|
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')
|
||||||
# 地理位置信息(需借助第三方API,这里只存储请求时的IP,后续可离线解析)
|
user_agent = models.CharField(max_length=500, blank=True, default='', verbose_name='User-Agent')
|
||||||
# 但我们可以存储更多请求细节
|
request_method = models.CharField(max_length=10, blank=True, default='', verbose_name='请求方法')
|
||||||
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_path = models.CharField(max_length=500, blank=True, default='', verbose_name='请求路径')
|
request_params = models.TextField(blank=True, default='', verbose_name='请求参数(GET/POST)')
|
||||||
request_params = models.TextField(blank=True, default='', verbose_name='请求参数(GET/POST)')
|
request_body = models.TextField(blank=True, default='', verbose_name='请求体(原始)')
|
||||||
request_body = models.TextField(blank=True, default='', verbose_name='请求体(原始)')
|
request_headers = models.TextField(blank=True, default='', verbose_name='请求头(JSON格式)')
|
||||||
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='详细描述')
|
||||||
# 攻击特征
|
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||||
attempt_type = models.CharField(max_length=50, blank=True, default='', verbose_name='攻击类型')
|
|
||||||
detail = models.TextField(blank=True, default='', verbose_name='详细描述')
|
class Meta:
|
||||||
|
db_table = 'abnormal_user_log'
|
||||||
# 时间戳
|
verbose_name = '异常用户日志'
|
||||||
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
verbose_name_plural = verbose_name
|
||||||
|
indexes = [
|
||||||
class Meta:
|
models.Index(fields=['account_id']),
|
||||||
db_table = 'abnormal_user_log'
|
models.Index(fields=['ip_address']),
|
||||||
verbose_name = '异常用户日志'
|
models.Index(fields=['CreateTime']),
|
||||||
verbose_name_plural = verbose_name
|
]
|
||||||
indexes = [
|
|
||||||
models.Index(fields=['account_id']),
|
def __str__(self):
|
||||||
models.Index(fields=['ip_address']),
|
return f"{self.account_id} @ {self.ip_address} - {self.CreateTime}"
|
||||||
models.Index(fields=['create_time']),
|
|
||||||
]
|
|
||||||
|
# ==================== 6. 商家每日统计 ====================
|
||||||
def __str__(self):
|
class MerchantDailyStats(QModel):
|
||||||
return f"{self.account_id} @ {self.ip_address} - {self.create_time}"
|
"""商家每日统计表(原 ShangjiaRiTongji)"""
|
||||||
|
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||||
|
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='日期')
|
||||||
class ShangjiaRiTongji(QModel):
|
|
||||||
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='商家用户ID')
|
# 派发统计
|
||||||
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
|
AssignedOrderCount = models.IntegerField(
|
||||||
|
default=0, db_column='paifa_dingdan_shu', verbose_name='派发订单总数')
|
||||||
# 派发统计
|
AssignedAmount = models.DecimalField(
|
||||||
paifa_dingdan_shu = models.IntegerField(default=0, verbose_name='派发订单总数')
|
max_digits=12, decimal_places=2, default=0.00,
|
||||||
paifa_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='派发订单总金额')
|
db_column='paifa_jine', verbose_name='派发订单总金额')
|
||||||
|
|
||||||
# 结算统计(成交)
|
# 结算统计
|
||||||
jiesuan_dingdan_shu = models.IntegerField(default=0, verbose_name='结算订单总数')
|
SettledOrderCount = models.IntegerField(
|
||||||
jiesuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='结算总金额')
|
default=0, db_column='jiesuan_dingdan_shu', verbose_name='结算订单总数')
|
||||||
|
SettledAmount = models.DecimalField(
|
||||||
# 退款统计(可选)
|
max_digits=12, decimal_places=2, default=0.00,
|
||||||
tuikuan_dingdan_shu = models.IntegerField(default=0, verbose_name='退款订单总数')
|
db_column='jiesuan_jine', verbose_name='结算总金额')
|
||||||
tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额')
|
|
||||||
|
# 退款统计
|
||||||
class Meta:
|
RefundOrderCount = models.IntegerField(
|
||||||
db_table = 'shangjia_ri_tongji'
|
default=0, db_column='tuikuan_dingdan_shu', verbose_name='退款订单总数')
|
||||||
verbose_name = '商家每日统计'
|
RefundAmount = models.DecimalField(
|
||||||
unique_together = [['yonghuid', 'riqi']] # 每人每天一条记录
|
max_digits=12, decimal_places=2, default=0.00,
|
||||||
indexes = [
|
db_column='tuikuan_jine', verbose_name='退款总金额')
|
||||||
models.Index(fields=['riqi', '-paifa_jine']),
|
|
||||||
models.Index(fields=['riqi', '-jiesuan_jine']),
|
class Meta:
|
||||||
models.Index(fields=['riqi', '-paifa_dingdan_shu']),
|
db_table = 'shangjia_ri_tongji'
|
||||||
]
|
verbose_name = '商家每日统计'
|
||||||
|
unique_together = [['club_id', 'MerchantID', 'Date']]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['Date', '-AssignedAmount']),
|
||||||
|
models.Index(fields=['Date', '-SettledAmount']),
|
||||||
|
models.Index(fields=['Date', '-AssignedOrderCount']),
|
||||||
class DashouRiTongji(QModel):
|
]
|
||||||
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='打手用户ID')
|
|
||||||
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
|
def __str__(self):
|
||||||
|
return f"商家{self.MerchantID} {self.Date}"
|
||||||
# 接单统计
|
|
||||||
jiedan_zongliang = models.IntegerField(default=0, verbose_name='接单总量')
|
|
||||||
chengjiao_zongliang = models.IntegerField(default=0, verbose_name='成交单总量')
|
# ==================== 7. 打手每日统计 ====================
|
||||||
|
class PlayerDailyStats(QModel):
|
||||||
# 金额统计
|
"""打手每日统计表(原 DashouRiTongji)"""
|
||||||
jiedan_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='接单总金额')
|
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||||
chengjiao_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='成交总金额')
|
PlayerID = models.CharField(
|
||||||
|
max_length=7, db_index=True, db_column='yonghuid', verbose_name='打手用户ID')
|
||||||
# 退款统计
|
Date = models.DateField(
|
||||||
tuikuan_liang = models.IntegerField(default=0, verbose_name='退款订单量')
|
db_index=True, db_column='riqi', verbose_name='日期')
|
||||||
tuikuan_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='退款总金额')
|
|
||||||
|
# 接单统计
|
||||||
class Meta:
|
AcceptedOrderTotal = models.IntegerField(
|
||||||
db_table = 'dashou_ri_tongji'
|
default=0, db_column='jiedan_zongliang', verbose_name='接单总量')
|
||||||
verbose_name = '打手每日统计'
|
CompletedOrderTotal = models.IntegerField(
|
||||||
unique_together = [['yonghuid', 'riqi']]
|
default=0, db_column='chengjiao_zongliang', verbose_name='成交单总量')
|
||||||
indexes = [
|
|
||||||
models.Index(fields=['riqi', '-jiedan_zongliang']),
|
# 金额统计
|
||||||
models.Index(fields=['riqi', '-chengjiao_zongliang']),
|
AcceptedAmount = models.DecimalField(
|
||||||
models.Index(fields=['riqi', '-jiedan_zonge']),
|
max_digits=12, decimal_places=2, default=0.00,
|
||||||
models.Index(fields=['riqi', '-chengjiao_zonge']),
|
db_column='jiedan_zonge', verbose_name='接单总金额')
|
||||||
]
|
CompletedAmount = models.DecimalField(
|
||||||
|
max_digits=12, decimal_places=2, default=0.00,
|
||||||
|
db_column='chengjiao_zonge', verbose_name='成交总金额')
|
||||||
|
|
||||||
class ZuzhangRiTongji(QModel):
|
# 退款统计
|
||||||
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='组长用户ID')
|
RefundCount = models.IntegerField(
|
||||||
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
|
default=0, db_column='tuikuan_liang', verbose_name='退款订单量')
|
||||||
|
RefundAmount = models.DecimalField(
|
||||||
yaoqing_guanshi_shu = models.IntegerField(default=0, verbose_name='邀请管事总数')
|
max_digits=12, decimal_places=2, default=0.00,
|
||||||
shouru_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额(分佣)')
|
db_column='tuikuan_jine', verbose_name='退款总金额')
|
||||||
|
|
||||||
# 可选明细
|
class Meta:
|
||||||
fenyong_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='分佣金额')
|
db_table = 'dashou_ri_tongji'
|
||||||
|
verbose_name = '打手每日统计'
|
||||||
class Meta:
|
unique_together = [['club_id', 'PlayerID', 'Date']]
|
||||||
db_table = 'zuzhang_ri_tongji'
|
indexes = [
|
||||||
verbose_name = '组长每日统计'
|
models.Index(fields=['Date', '-AcceptedOrderTotal']),
|
||||||
unique_together = [['yonghuid', 'riqi']]
|
models.Index(fields=['Date', '-CompletedOrderTotal']),
|
||||||
indexes = [
|
models.Index(fields=['Date', '-AcceptedAmount']),
|
||||||
models.Index(fields=['riqi', '-yaoqing_guanshi_shu']),
|
models.Index(fields=['Date', '-CompletedAmount']),
|
||||||
models.Index(fields=['riqi', '-shouru_zonge']),
|
]
|
||||||
]
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"打手{self.PlayerID} {self.Date}"
|
||||||
|
|
||||||
|
|
||||||
class GuanshiRiTongji(QModel):
|
# ==================== 8. 组长每日统计 ====================
|
||||||
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID')
|
class LeaderDailyStats(QModel):
|
||||||
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
|
"""组长每日统计表(原 ZuzhangRiTongji)"""
|
||||||
|
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||||
yaoqing_dashou_shu = models.IntegerField(default=0, verbose_name='邀请打手总数')
|
LeaderID = models.CharField(
|
||||||
chongzhi_dashou_shu = models.IntegerField(default=0, verbose_name='充值打手数量')
|
max_length=7, db_index=True, db_column='yonghuid', verbose_name='组长用户ID')
|
||||||
shouru_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='收入总额')
|
Date = models.DateField(
|
||||||
|
db_index=True, db_column='riqi', verbose_name='日期')
|
||||||
class Meta:
|
|
||||||
db_table = 'guanshi_ri_tongji'
|
InvitedManagerCount = models.IntegerField(
|
||||||
verbose_name = '管事每日统计'
|
default=0, db_column='yaoqing_guanshi_shu', verbose_name='邀请管事总数')
|
||||||
unique_together = [['yonghuid', 'riqi']]
|
TotalIncome = models.DecimalField(
|
||||||
indexes = [
|
max_digits=12, decimal_places=2, default=0.00,
|
||||||
models.Index(fields=['riqi', '-yaoqing_dashou_shu']),
|
db_column='shouru_zonge', verbose_name='收入总额(分佣)')
|
||||||
models.Index(fields=['riqi', '-chongzhi_dashou_shu']),
|
CommissionAmount = models.DecimalField(
|
||||||
models.Index(fields=['riqi', '-shouru_zonge']),
|
max_digits=12, decimal_places=2, default=0.00,
|
||||||
]
|
db_column='fenyong_jine', verbose_name='分佣金额')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'zuzhang_ri_tongji'
|
||||||
|
verbose_name = '组长每日统计'
|
||||||
class GuanshiXufeiRiTongji(QModel):
|
unique_together = [['club_id', 'LeaderID', 'Date']]
|
||||||
yonghuid = models.CharField(max_length=7, db_index=True, verbose_name='管事用户ID')
|
indexes = [
|
||||||
riqi = models.DateField(db_index=True, verbose_name='日期') # 年月日
|
models.Index(fields=['Date', '-InvitedManagerCount']),
|
||||||
|
models.Index(fields=['Date', '-TotalIncome']),
|
||||||
xufei_zongshu = models.IntegerField(default=0, verbose_name='今日续费总数') # 续费订单数(或续费打手数)
|
]
|
||||||
xufei_shouyi = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='续费收益金额')
|
|
||||||
|
def __str__(self):
|
||||||
class Meta:
|
return f"组长{self.LeaderID} {self.Date}"
|
||||||
db_table = 'guanshi_xufei_ri_tongji'
|
|
||||||
verbose_name = '管事续费每日统计'
|
|
||||||
unique_together = [['yonghuid', 'riqi']]
|
# ==================== 9. 管事每日统计 ====================
|
||||||
indexes = [
|
class ManagerDailyStats(QModel):
|
||||||
models.Index(fields=['riqi', '-xufei_zongshu']),
|
"""管事每日统计表(原 GuanshiRiTongji)"""
|
||||||
models.Index(fields=['riqi', '-xufei_shouyi']),
|
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||||
]
|
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='日期')
|
||||||
# models.py 中添加
|
|
||||||
|
InvitedPlayerCount = models.IntegerField(
|
||||||
class TixianRiTongji(QModel):
|
default=0, db_column='yaoqing_dashou_shu', verbose_name='邀请打手总数')
|
||||||
"""
|
RechargedPlayerCount = models.IntegerField(
|
||||||
每日提现统计表(按身份统计当天提现总额)
|
default=0, db_column='chongzhi_dashou_shu', verbose_name='充值打手数量')
|
||||||
"""
|
TotalIncome = models.DecimalField(
|
||||||
riqi = models.DateField(db_index=True, verbose_name='日期')
|
max_digits=12, decimal_places=2, default=0.00,
|
||||||
leixing = models.IntegerField(
|
db_column='shouru_zonge', verbose_name='收入总额')
|
||||||
choices=[(1, '打手'), (2, '管事'), (3, '组长'),
|
|
||||||
(4, '考核官'), (5, '打手押金'), (6, '商家余额')],
|
class Meta:
|
||||||
verbose_name='用户类型'
|
db_table = 'guanshi_ri_tongji'
|
||||||
)
|
verbose_name = '管事每日统计'
|
||||||
total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='当天提现总额(元)')
|
unique_together = [['club_id', 'ManagerID', 'Date']]
|
||||||
|
indexes = [
|
||||||
class Meta:
|
models.Index(fields=['Date', '-InvitedPlayerCount']),
|
||||||
db_table = 'tixian_ri_tongji'
|
models.Index(fields=['Date', '-RechargedPlayerCount']),
|
||||||
verbose_name = '每日提现统计'
|
models.Index(fields=['Date', '-TotalIncome']),
|
||||||
unique_together = [['riqi', 'leixing']] # 每天每个身份一条记录
|
]
|
||||||
indexes = [models.Index(fields=['riqi', 'leixing'])]
|
|
||||||
|
def __str__(self):
|
||||||
def __str__(self):
|
return f"管事{self.ManagerID} {self.Date}"
|
||||||
return f"{self.riqi} - {self.get_leixing_display()} 提现总额: {self.total_amount}"
|
|
||||||
|
|
||||||
|
# ==================== 10. 管事续费每日统计 ====================
|
||||||
|
class ManagerRenewalDailyStats(QModel):
|
||||||
|
"""管事续费每日统计表(原 GuanshiXufeiRiTongji)"""
|
||||||
|
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||||
|
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='日期')
|
||||||
|
|
||||||
|
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 = [['club_id', 'ManagerID', 'Date']]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['Date', '-RenewalTotal']),
|
||||||
|
models.Index(fields=['Date', '-RenewalRevenue']),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"管事续费{self.ManagerID} {self.Date}"
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 11. 提现每日统计 ====================
|
||||||
|
class WithdrawalDailyStats(QModel):
|
||||||
|
"""每日提现统计表(原 TixianRiTongji),按俱乐部+身份统计当天提现到账总额"""
|
||||||
|
club_id = models.CharField(max_length=16, default='xq', db_index=True, verbose_name='俱乐部ID')
|
||||||
|
Date = models.DateField(
|
||||||
|
db_index=True, db_column='riqi', verbose_name='日期')
|
||||||
|
WithdrawalType = models.IntegerField(
|
||||||
|
choices=[(1, '打手'), (2, '管事'), (3, '组长'),
|
||||||
|
(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 = [['club_id', 'Date', 'WithdrawalType']]
|
||||||
|
indexes = [
|
||||||
|
models.Index(fields=['club_id', 'Date', 'WithdrawalType']),
|
||||||
|
models.Index(fields=['Date', 'WithdrawalType']),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.Date} - {self.get_WithdrawalType_display()} 提现总额: {self.total_amount}"
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 兼容别名(供逐步迁移) ====================
|
||||||
|
ShangjiaRiTongji = MerchantDailyStats
|
||||||
|
DashouRiTongji = PlayerDailyStats
|
||||||
|
ZuzhangRiTongji = LeaderDailyStats
|
||||||
|
GuanshiRiTongji = ManagerDailyStats
|
||||||
|
GuanshiXufeiRiTongji = ManagerRenewalDailyStats
|
||||||
|
TixianRiTongji = WithdrawalDailyStats
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class PopupPageSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = PopupPage
|
model = PopupPage
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'page_key', 'name', 'description',
|
'id', 'club_id', 'page_key', 'name', 'description',
|
||||||
'is_active', 'ignore_user_mute',
|
'is_active', 'ignore_user_mute',
|
||||||
'created_at', 'updated_at', 'popups'
|
'created_at', 'updated_at', 'popups'
|
||||||
]
|
]
|
||||||
@@ -6,15 +6,17 @@ from .view import (GetRolePermissionView,
|
|||||||
ModifyAdminUserView, KefuGetDashouListView, KefuGetDashouDetailView, KefuUpdateDashouView, KefuGetShangjiaListView,
|
ModifyAdminUserView, KefuGetDashouListView, KefuGetDashouDetailView, KefuUpdateDashouView, KefuGetShangjiaListView,
|
||||||
KefuGetShangjiaDetailView, KefuUpdateShangjiaView, GetProductBaseDataView, GetProductListView, SaveProductOrderView,
|
KefuGetShangjiaDetailView, KefuUpdateShangjiaView, GetProductBaseDataView, GetProductListView, SaveProductOrderView,
|
||||||
AddProductView, DeleteProductView, GetProductDetailView, UpdateProductView, UpdateMemberView, GetMemberListView,
|
AddProductView, DeleteProductView, GetProductDetailView, UpdateProductView, UpdateMemberView, GetMemberListView,
|
||||||
AddMemberView, GetGuanliListView, GetGuanliDetailView, UpdateGuanliView, GetWithdrawSettingsView,
|
AddMemberView, GetGuanliListView, GetGuanliDetailView, PromoteGuanshiToZuzhangView, ChangeInviterView, UpdateGuanliView, GetWithdrawSettingsView,
|
||||||
UpdateWithdrawSettingsView, GetZuzhangListView, GetZuzhangDetailView, UpdateZuzhangView, GetProductTypeZoneView,
|
UpdateWithdrawSettingsView, GetZuzhangListView, GetZuzhangDetailView, UpdateZuzhangView, GetOperationLogListView, GetProductTypeZoneView,
|
||||||
ModifyProductTypeZoneView, GetRateView, ModifyRateView, PopupNoticeListAPIView, PopupNoticeModifyAPIView,ShopListView,
|
ModifyProductTypeZoneView, GetRateView, ModifyRateView, PopupNoticeListAPIView, PopupNoticeModifyAPIView,ShopListView,
|
||||||
ShopModifyView, ShopPublicTypeAndAuditView, ShopListForProductView, ShopProductModifyView,
|
ShopModifyView, ShopPublicTypeAndAuditView, ShopListForProductView, ShopProductModifyView,
|
||||||
ShopProductTypeMappingView, ProductListView, ProductModifyView, KefuChatPermissionsView,
|
ShopProductTypeMappingView, ProductListView, ProductModifyView, KefuChatPermissionsView,
|
||||||
FineApplyView, PunishDashouView,
|
FineApplyView, PunishDashouView,
|
||||||
FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView,
|
FaKuanTongJiView, FaKuanLieBiaoView, FaKuanChuLiView, FaKuanChuangJianView, FaKuanPingTaiShenHeView,
|
||||||
HqbkxxView,BkxgView,CaiwuView,CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView,
|
HqbkxxView,BkxgView,CaiwuView,CwhybkhqView, HybkjtsjView, SzxxView, CwddhqlxView, CwhqjtddsjView,
|
||||||
CwqtczhqView, KhpzhqView, ChzsgcView,KhgglView, ShgxgsjView, ZxkfghdsView)
|
CwqtczhqView, KhpzhqView, ChzsgcView,KhgglView, ShgxgsjView, KhjlglView, KhjlczView, ZxkfghdsView)
|
||||||
|
from config.views import MiniappScriptHoutaiListView, MiniappScriptHoutaiModifyView
|
||||||
|
from rank.reward_houtai_views import RankRewardHoutaiListView, RankRewardHoutaiSaveView, RankRewardHoutaiClaimsView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('gljshq', GetRolePermissionView.as_view(), name='获取就角色信息'),
|
path('gljshq', GetRolePermissionView.as_view(), name='获取就角色信息'),
|
||||||
@@ -46,7 +48,10 @@ urlpatterns = [
|
|||||||
path('hthqgslb', GetGuanliListView.as_view(), name='后台获取管事列表'),
|
path('hthqgslb', GetGuanliListView.as_view(), name='后台获取管事列表'),
|
||||||
|
|
||||||
path('hthqgsxq', GetGuanliDetailView.as_view(), name='后台获取管事详情信息'),
|
path('hthqgsxq', GetGuanliDetailView.as_view(), name='后台获取管事详情信息'),
|
||||||
|
path('htgssjzz', PromoteGuanshiToZuzhangView.as_view(), name='后台管事升级组长'),
|
||||||
|
path('htghyqr', ChangeInviterView.as_view(), name='后台更换邀请人'),
|
||||||
path('htxggsxx', UpdateGuanliView.as_view(), name='后台更新管事数据'),
|
path('htxggsxx', UpdateGuanliView.as_view(), name='后台更新管事数据'),
|
||||||
|
path('hqczrz', GetOperationLogListView.as_view(), name='后台操作日志列表'),
|
||||||
path('hthqtxpz', GetWithdrawSettingsView.as_view(), name='后台获取提现配置'),
|
path('hthqtxpz', GetWithdrawSettingsView.as_view(), name='后台获取提现配置'),
|
||||||
path('htxgtxsz', UpdateWithdrawSettingsView.as_view(), name='后台修改提现配置'),
|
path('htxgtxsz', UpdateWithdrawSettingsView.as_view(), name='后台修改提现配置'),
|
||||||
path('hthqzzlb', GetZuzhangListView.as_view(), name='后台获取组长列表'),
|
path('hthqzzlb', GetZuzhangListView.as_view(), name='后台获取组长列表'),
|
||||||
@@ -60,6 +65,11 @@ urlpatterns = [
|
|||||||
path('htxgfl', ModifyRateView.as_view(), name='后台修改费率'),
|
path('htxgfl', ModifyRateView.as_view(), name='后台修改费率'),
|
||||||
path('hthqtcxx', PopupNoticeListAPIView.as_view(), name='后台获取弹窗配置'),
|
path('hthqtcxx', PopupNoticeListAPIView.as_view(), name='后台获取弹窗配置'),
|
||||||
path('htxgtcxx', PopupNoticeModifyAPIView.as_view(), name='后台更新创建弹窗配置'),
|
path('htxgtcxx', PopupNoticeModifyAPIView.as_view(), name='后台更新创建弹窗配置'),
|
||||||
|
path('hthqhs', MiniappScriptHoutaiListView.as_view(), name='后台获取话术配置'),
|
||||||
|
path('htxghs', MiniappScriptHoutaiModifyView.as_view(), name='后台更新话术配置'),
|
||||||
|
path('phbjhq', RankRewardHoutaiListView.as_view(), name='后台排行榜奖励配置'),
|
||||||
|
path('phbjbc', RankRewardHoutaiSaveView.as_view(), name='后台保存排行榜奖励'),
|
||||||
|
path('phbjjl', RankRewardHoutaiClaimsView.as_view(), name='后台排行榜领取明细'),
|
||||||
|
|
||||||
path('hqdplbsj', ShopListView.as_view(), name='获取店铺列表数据'),
|
path('hqdplbsj', ShopListView.as_view(), name='获取店铺列表数据'),
|
||||||
path('xgdpxx', ShopModifyView.as_view(), name='修改或添加店铺数据'),
|
path('xgdpxx', ShopModifyView.as_view(), name='修改或添加店铺数据'),
|
||||||
@@ -79,6 +89,7 @@ urlpatterns = [
|
|||||||
path('htglyhqcfsltj', FaKuanTongJiView.as_view(),name='后台罚单统计图片'), # 罚款统计
|
path('htglyhqcfsltj', FaKuanTongJiView.as_view(),name='后台罚单统计图片'), # 罚款统计
|
||||||
path('hthqfklb', FaKuanLieBiaoView.as_view(),name='后台罚单列表'), # 罚款列表
|
path('hthqfklb', FaKuanLieBiaoView.as_view(),name='后台罚单列表'), # 罚款列表
|
||||||
path('glyclfk', FaKuanChuLiView.as_view(),name='后台罚单操作'), # 处理罚款
|
path('glyclfk', FaKuanChuLiView.as_view(),name='后台罚单操作'), # 处理罚款
|
||||||
|
path('glyptshfk', FaKuanPingTaiShenHeView.as_view(), name='后台罚款平台审核'),
|
||||||
path('htfksc', FaKuanChuangJianView.as_view(),name='后台生成罚单'), # 创建罚款
|
path('htfksc', FaKuanChuangJianView.as_view(),name='后台生成罚单'), # 创建罚款
|
||||||
path('kffkdssq', FineApplyView.as_view(), name='客服罚款申请'), # 客服罚款申请
|
path('kffkdssq', FineApplyView.as_view(), name='客服罚款申请'), # 客服罚款申请
|
||||||
path('kpcf', PunishDashouView.as_view(), name='客服处罚打手'), # 客服处罚打手
|
path('kpcf', PunishDashouView.as_view(), name='客服处罚打手'), # 客服处罚打手
|
||||||
@@ -100,6 +111,8 @@ urlpatterns = [
|
|||||||
|
|
||||||
path('khggl', KhgglView.as_view(), name='获取考核管信息'),
|
path('khggl', KhgglView.as_view(), name='获取考核管信息'),
|
||||||
path('shgxgsj', ShgxgsjView.as_view(), name='修改考核管数据'),
|
path('shgxgsj', ShgxgsjView.as_view(), name='修改考核管数据'),
|
||||||
|
path('khjlgl', KhjlglView.as_view(), name='考核记录列表与统计'),
|
||||||
|
path('khjlcz', KhjlczView.as_view(), name='考核记录操作'),
|
||||||
|
|
||||||
path('zxkfghds', ZxkfghdsView.as_view(), name='链接换打手或重新下单'),
|
path('zxkfghds', ZxkfghdsView.as_view(), name='链接换打手或重新下单'),
|
||||||
|
|
||||||
|
|||||||
1160
backend/utils.py
1160
backend/utils.py
File diff suppressed because it is too large
Load Diff
8497
backend/view.py
8497
backend/view.py
File diff suppressed because it is too large
Load Diff
744
backend/views/exams.py
Normal file
744
backend/views/exams.py
Normal file
@@ -0,0 +1,744 @@
|
|||||||
|
import hmac
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import xmltodict
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from calendar import monthrange
|
||||||
|
from decimal import Decimal
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# 工具类导入
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action,
|
||||||
|
update_shangjia_daily
|
||||||
|
)
|
||||||
|
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||||
|
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||||
|
from jituan.services.club_penalty import (
|
||||||
|
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||||
|
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||||
|
)
|
||||||
|
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
from ..utils import (
|
||||||
|
verify_kefu_permission,
|
||||||
|
PERMISSION_TO_SHENFEN,
|
||||||
|
SHENFEN_PROFILE_MAP,
|
||||||
|
has_fadan_view_permission,
|
||||||
|
has_merchant_order_permission,
|
||||||
|
check_fadan_permission,
|
||||||
|
pick_order_id,
|
||||||
|
pick_penalty_create_params,
|
||||||
|
write_xiugai_log,
|
||||||
|
XIUGAI_LEIXING_DASHOU,
|
||||||
|
XIUGAI_LEIXING_GUANSHI,
|
||||||
|
XIUGAI_LEIXING_SHANGJIA,
|
||||||
|
XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
XIUGAI_LEIXING_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# models 集中导入
|
||||||
|
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||||
|
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||||
|
## backend
|
||||||
|
from backend.models import WithdrawalDailyStats
|
||||||
|
|
||||||
|
## users
|
||||||
|
from users.models import (
|
||||||
|
UserGuanshi, UserBoss, UserZuzhang,
|
||||||
|
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
## orders
|
||||||
|
from orders.models import (
|
||||||
|
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||||
|
OrderPlayerHistory, Order, RefundRecord,
|
||||||
|
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||||
|
)
|
||||||
|
|
||||||
|
## shop
|
||||||
|
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||||
|
|
||||||
|
## products
|
||||||
|
from products.models import (
|
||||||
|
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||||
|
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||||
|
)
|
||||||
|
|
||||||
|
## config
|
||||||
|
from config.models import (
|
||||||
|
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||||
|
TixianQuotaDefault,
|
||||||
|
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||||
|
)
|
||||||
|
|
||||||
|
## rank
|
||||||
|
from rank.models import (
|
||||||
|
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||||
|
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||||
|
)
|
||||||
|
|
||||||
|
# 序列化器
|
||||||
|
from ..serializers import PopupPageSerializer
|
||||||
|
|
||||||
|
# 全局常量、日志对象
|
||||||
|
logger = logging.getLogger('houtai')
|
||||||
|
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||||
|
|
||||||
|
# 审核记录状态映射(原位于 zxkf.py,按功能组归类到考核模块)
|
||||||
|
SHENHE_ZHUANGTAI_MAP = {
|
||||||
|
0: '考核中',
|
||||||
|
1: '已通过',
|
||||||
|
2: '未通过',
|
||||||
|
3: '待审核',
|
||||||
|
}
|
||||||
|
|
||||||
|
class KhgglView(APIView):
|
||||||
|
"""
|
||||||
|
考核官管理数据接口(列表、筛选、分页)
|
||||||
|
POST /houtai/khggl
|
||||||
|
Body: {
|
||||||
|
"phone": "管理员手机号",
|
||||||
|
"page": 1, // 可选,默认1
|
||||||
|
"page_size": 15, // 可选,默认15
|
||||||
|
"yonghuid": "1234567", // 按用户ID筛选,可选
|
||||||
|
"zhuangtai": 1, // 账号状态 0=禁用,1=正常,可选
|
||||||
|
"shenhe_zhuangtai": 0, // 审核官状态 0=空闲,1=审核中,可选
|
||||||
|
"bankuai_name": "王者" // 板块名称模糊筛选,可选
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
if 'kaohepeizhi' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
|
||||||
|
|
||||||
|
# 分页参数
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('page_size', 15))
|
||||||
|
if page < 1: page = 1
|
||||||
|
if page_size < 1: page_size = 15
|
||||||
|
|
||||||
|
# 筛选条件
|
||||||
|
filter_yonghuid = request.data.get('yonghuid', '').strip()
|
||||||
|
filter_zhuangtai = request.data.get('zhuangtai')
|
||||||
|
filter_shenhe_zhuangtai = request.data.get('shenhe_zhuangtai')
|
||||||
|
filter_bankuai_name = request.data.get('bankuai_name', '').strip()
|
||||||
|
|
||||||
|
# 基础查询:所有审核官,预加载用户及打手扩展表
|
||||||
|
queryset = filter_user_related_by_club(
|
||||||
|
UserShenheguan.query.select_related('user__DashouProfile'),
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
|
||||||
|
if filter_yonghuid:
|
||||||
|
queryset = queryset.filter(user__UserUID=filter_yonghuid)
|
||||||
|
if filter_zhuangtai is not None:
|
||||||
|
queryset = queryset.filter(zhuangtai=filter_zhuangtai)
|
||||||
|
if filter_shenhe_zhuangtai is not None:
|
||||||
|
queryset = queryset.filter(shenhe_zhuangtai=filter_shenhe_zhuangtai)
|
||||||
|
|
||||||
|
# 如果填写了板块名称,筛选出关联该板块的审核官
|
||||||
|
if filter_bankuai_name:
|
||||||
|
bankuai_ids = Bankuai.query.filter(
|
||||||
|
mingcheng__icontains=filter_bankuai_name
|
||||||
|
).values_list('bankuai_id', flat=True)
|
||||||
|
shenheguan_ids = KaoheguanBankuai.query.filter(
|
||||||
|
bankuai_id__in=bankuai_ids
|
||||||
|
).values_list('kaoheguan_id', flat=True)
|
||||||
|
queryset = queryset.filter(user__UserUID__in=shenheguan_ids)
|
||||||
|
|
||||||
|
total = queryset.count()
|
||||||
|
start = (page - 1) * page_size
|
||||||
|
end = start + page_size
|
||||||
|
shenheguan_list = queryset.order_by('-CreateTime')[start:end]
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from jituan.services.shenhe_club import yonghu_chenghao_qs_for_request
|
||||||
|
|
||||||
|
# ---------- 优化:板块打手数量(1 次 annotate 替代 2N 次循环查询)----------
|
||||||
|
# 原逻辑:每板块查 dashou 标签 ID,再统计关联的不同用户数(要求有 DashouProfile)
|
||||||
|
bankuai_dashou_qs = yonghu_chenghao_qs_for_request(
|
||||||
|
YonghuChenghao.query.filter(
|
||||||
|
chenghao__leixing='dashou',
|
||||||
|
yonghu__DashouProfile__isnull=False,
|
||||||
|
),
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
bankuai_dashou_count_map = {
|
||||||
|
item['chenghao__bankuai_id']: item['user_count']
|
||||||
|
for item in bankuai_dashou_qs.values('chenghao__bankuai_id').annotate(
|
||||||
|
user_count=Count('yonghu', distinct=True)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 优化:板块标签统计(1 次 annotate 替代 N*M 次循环查询)----------
|
||||||
|
# 原逻辑:每板块每标签分别统计关联的不同用户数(不要求 DashouProfile)
|
||||||
|
tag_counts_qs = yonghu_chenghao_qs_for_request(
|
||||||
|
YonghuChenghao.query.filter(chenghao__leixing='dashou'),
|
||||||
|
request,
|
||||||
|
).values(
|
||||||
|
'chenghao_id', 'chenghao__bankuai_id', 'chenghao__mingcheng'
|
||||||
|
).annotate(user_count=Count('yonghu', distinct=True))
|
||||||
|
|
||||||
|
tags_by_bankuai = defaultdict(list)
|
||||||
|
for item in tag_counts_qs:
|
||||||
|
tags_by_bankuai[item['chenghao__bankuai_id']].append({
|
||||||
|
'id': item['chenghao_id'],
|
||||||
|
'name': item['chenghao__mingcheng'],
|
||||||
|
'user_count': item['user_count'],
|
||||||
|
})
|
||||||
|
|
||||||
|
all_banquai = Bankuai.query.all()
|
||||||
|
bankuai_tags_data = [
|
||||||
|
{
|
||||||
|
'bankuai_id': bk.bankuai_id,
|
||||||
|
'mingcheng': bk.mingcheng,
|
||||||
|
'tags': tags_by_bankuai.get(bk.bankuai_id, []),
|
||||||
|
}
|
||||||
|
for bk in all_banquai
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------- 优化:审核官板块关联(1 次批量查询替代 P 次循环查询)----------
|
||||||
|
shenheguan_user_uids = [sg.user.UserUID for sg in shenheguan_list]
|
||||||
|
all_kgs = KaoheguanBankuai.query.filter(
|
||||||
|
kaoheguan_id__in=shenheguan_user_uids
|
||||||
|
).select_related('bankuai')
|
||||||
|
kg_map = defaultdict(list)
|
||||||
|
for kg in all_kgs:
|
||||||
|
kg_map[kg.kaoheguan_id].append(kg)
|
||||||
|
|
||||||
|
# 构造返回数据(审核官列表)
|
||||||
|
data_list = []
|
||||||
|
for sg in shenheguan_list:
|
||||||
|
user = sg.user
|
||||||
|
bankuai_info = [
|
||||||
|
{
|
||||||
|
'bankuai_id': kg.bankuai.bankuai_id,
|
||||||
|
'mingcheng': kg.bankuai.mingcheng,
|
||||||
|
'dashou_count': bankuai_dashou_count_map.get(kg.bankuai.bankuai_id, 0),
|
||||||
|
}
|
||||||
|
for kg in kg_map.get(user.UserUID, [])
|
||||||
|
]
|
||||||
|
|
||||||
|
dashou_nick = user.DashouProfile.nicheng if hasattr(user, 'DashouProfile') and user.DashouProfile else ''
|
||||||
|
|
||||||
|
data_list.append({
|
||||||
|
'yonghuid': user.UserUID,
|
||||||
|
'avatar': user.Avatar or '',
|
||||||
|
'phone': user.Phone or '',
|
||||||
|
'zhuangtai': sg.zhuangtai,
|
||||||
|
'shenhe_zhuangtai': sg.shenhe_zhuangtai,
|
||||||
|
'shenhe_zongshu': sg.shenhe_zongshu,
|
||||||
|
'tongguo_zongshu': sg.tongguo_zongshu,
|
||||||
|
'yue': float(sg.yue),
|
||||||
|
'zonge': float(sg.zonge),
|
||||||
|
'CreateTime': sg.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.CreateTime else '',
|
||||||
|
'UpdateTime': sg.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.UpdateTime else '',
|
||||||
|
'bankuai': bankuai_info,
|
||||||
|
'nicheng': dashou_nick,
|
||||||
|
'is_renzheng': sg.is_renzheng,
|
||||||
|
})
|
||||||
|
|
||||||
|
all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'ok',
|
||||||
|
'data': {
|
||||||
|
'total': total,
|
||||||
|
'page': page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'list': data_list,
|
||||||
|
'all_bankuai': all_bankuai,
|
||||||
|
'bankuai_tags': bankuai_tags_data, # 新增板块标签统计
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("KhgglView 接口错误")
|
||||||
|
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||||
|
|
||||||
|
class ShgxgsjView(APIView):
|
||||||
|
"""
|
||||||
|
审核官信息修改(余额、状态、板块关联)
|
||||||
|
POST /houtai/shgxgsj
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
if 'kaohepeizhi' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
|
||||||
|
|
||||||
|
yonghuid = request.data.get('yonghuid')
|
||||||
|
if not yonghuid:
|
||||||
|
return Response({'code': 1, 'msg': '缺少用户ID'}, status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
shenheguan = UserShenheguan.query.select_related('user').get(user__UserUID=yonghuid)
|
||||||
|
except UserShenheguan.DoesNotExist:
|
||||||
|
return Response({'code': 1, 'msg': '审核官不存在'}, status=404)
|
||||||
|
|
||||||
|
from jituan.services.shenhe_club import user_belongs_to_request_club
|
||||||
|
if not user_belongs_to_request_club(shenheguan.user, request):
|
||||||
|
return Response({'code': 403, 'msg': '该考核官不属于当前俱乐部'}, status=403)
|
||||||
|
|
||||||
|
action = request.data.get('action')
|
||||||
|
|
||||||
|
if action == 'update_info':
|
||||||
|
if 'yue' in request.data:
|
||||||
|
shenheguan.yue = float(request.data.get('yue', 0))
|
||||||
|
if 'zhuangtai' in request.data:
|
||||||
|
zt = int(request.data.get('zhuangtai'))
|
||||||
|
if zt not in [0, 1]:
|
||||||
|
return Response({'code': 1, 'msg': '无效的账号状态'}, status=400)
|
||||||
|
shenheguan.zhuangtai = zt
|
||||||
|
if 'shenhe_zhuangtai' in request.data:
|
||||||
|
szt = int(request.data.get('shenhe_zhuangtai'))
|
||||||
|
if szt not in [0, 1]:
|
||||||
|
return Response({'code': 1, 'msg': '无效的审核官状态'}, status=400)
|
||||||
|
shenheguan.shenhe_zhuangtai = szt
|
||||||
|
# 新增认证状态编辑
|
||||||
|
if 'is_renzheng' in request.data:
|
||||||
|
shenheguan.is_renzheng = bool(request.data.get('is_renzheng'))
|
||||||
|
shenheguan.save()
|
||||||
|
return Response({'code': 0, 'msg': '修改成功'})
|
||||||
|
|
||||||
|
elif action == 'add_bankuai':
|
||||||
|
bankuai_id = request.data.get('bankuai_id')
|
||||||
|
if not bankuai_id:
|
||||||
|
return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
|
||||||
|
if not Bankuai.query.filter(bankuai_id=bankuai_id).exists():
|
||||||
|
return Response({'code': 1, 'msg': '板块不存在'}, status=400)
|
||||||
|
if KaoheguanBankuai.query.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists():
|
||||||
|
return Response({'code': 1, 'msg': '已关联该板块'}, status=400)
|
||||||
|
KaoheguanBankuai.query.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id)
|
||||||
|
return Response({'code': 0, 'msg': '板块关联成功'})
|
||||||
|
|
||||||
|
elif action == 'remove_bankuai':
|
||||||
|
bankuai_id = request.data.get('bankuai_id')
|
||||||
|
if not bankuai_id:
|
||||||
|
return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
|
||||||
|
deleted, _ = KaoheguanBankuai.query.filter(
|
||||||
|
kaoheguan_id=yonghuid, bankuai_id=bankuai_id
|
||||||
|
).delete()
|
||||||
|
if not deleted:
|
||||||
|
return Response({'code': 1, 'msg': '未找到该板块关联'}, status=400)
|
||||||
|
return Response({'code': 0, 'msg': '板块移除成功'})
|
||||||
|
|
||||||
|
else:
|
||||||
|
return Response({'code': 1, 'msg': '未知操作'}, status=400)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("ShgxgsjView 接口错误")
|
||||||
|
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _batch_user_profile_map(user_uids):
|
||||||
|
"""批量获取用户头像、昵称、手机号"""
|
||||||
|
uids = list({u for u in user_uids if u})
|
||||||
|
if not uids:
|
||||||
|
return {}
|
||||||
|
users = User.query.filter(UserUID__in=uids)
|
||||||
|
dashou_nicks = {
|
||||||
|
d.user.UserUID: d.nicheng
|
||||||
|
for d in UserDashou.query.filter(user__UserUID__in=uids).select_related('user')
|
||||||
|
}
|
||||||
|
result = {}
|
||||||
|
for u in users:
|
||||||
|
result[u.UserUID] = {
|
||||||
|
'yonghuid': u.UserUID,
|
||||||
|
'avatar': u.Avatar or '',
|
||||||
|
'phone': u.Phone or '',
|
||||||
|
'nicheng': dashou_nicks.get(u.UserUID) or u.UserName or u.Phone or u.UserUID,
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _build_shenhe_jilu_filters(request_data):
|
||||||
|
"""构造 ShenheJilu 查询条件(列表与统计共用)"""
|
||||||
|
qs = ShenheJilu.query.select_related('bankuai', 'chenghao')
|
||||||
|
|
||||||
|
jilu_id = (request_data.get('jilu_id') or '').strip()
|
||||||
|
if jilu_id:
|
||||||
|
qs = qs.filter(jilu_id=jilu_id)
|
||||||
|
|
||||||
|
zhuangtai = request_data.get('zhuangtai')
|
||||||
|
if zhuangtai is not None and zhuangtai != '':
|
||||||
|
qs = qs.filter(zhuangtai=int(zhuangtai))
|
||||||
|
|
||||||
|
assessed = request_data.get('assessed')
|
||||||
|
if assessed == 'done':
|
||||||
|
qs = qs.filter(zhuangtai__in=[1, 2])
|
||||||
|
elif assessed == 'undone':
|
||||||
|
qs = qs.filter(zhuangtai__in=[0, 3])
|
||||||
|
|
||||||
|
bankuai_id = request_data.get('bankuai_id')
|
||||||
|
if bankuai_id not in (None, ''):
|
||||||
|
qs = qs.filter(bankuai_id=int(bankuai_id))
|
||||||
|
|
||||||
|
chenghao_id = request_data.get('chenghao_id')
|
||||||
|
if chenghao_id not in (None, ''):
|
||||||
|
qs = qs.filter(chenghao_id=int(chenghao_id))
|
||||||
|
|
||||||
|
cishu = request_data.get('cishu')
|
||||||
|
if cishu not in (None, ''):
|
||||||
|
qs = qs.filter(cishu=int(cishu))
|
||||||
|
|
||||||
|
dashou_kw = (request_data.get('dashou_keyword') or '').strip()
|
||||||
|
if dashou_kw:
|
||||||
|
dashou_uids = list(
|
||||||
|
User.query.filter(
|
||||||
|
Q(UserUID=dashou_kw) |
|
||||||
|
Q(UserName__icontains=dashou_kw) |
|
||||||
|
Q(Phone__icontains=dashou_kw) |
|
||||||
|
Q(DashouProfile__nicheng__icontains=dashou_kw)
|
||||||
|
).values_list('UserUID', flat=True)
|
||||||
|
)
|
||||||
|
if dashou_uids:
|
||||||
|
qs = qs.filter(shenqingren_id__in=dashou_uids)
|
||||||
|
else:
|
||||||
|
qs = qs.none()
|
||||||
|
|
||||||
|
kg_kw = (request_data.get('kaoheguan_keyword') or '').strip()
|
||||||
|
if kg_kw:
|
||||||
|
kg_uids = list(
|
||||||
|
User.query.filter(
|
||||||
|
Q(UserUID=kg_kw) |
|
||||||
|
Q(UserName__icontains=kg_kw) |
|
||||||
|
Q(Phone__icontains=kg_kw) |
|
||||||
|
Q(DashouProfile__nicheng__icontains=kg_kw)
|
||||||
|
).values_list('UserUID', flat=True)
|
||||||
|
)
|
||||||
|
if kg_uids:
|
||||||
|
qs = qs.filter(shenheguan_id__in=kg_uids)
|
||||||
|
else:
|
||||||
|
qs = qs.none()
|
||||||
|
|
||||||
|
start_time = (request_data.get('start_time') or '').strip()
|
||||||
|
end_time = (request_data.get('end_time') or '').strip()
|
||||||
|
if start_time:
|
||||||
|
qs = qs.filter(CreateTime__gte=start_time)
|
||||||
|
if end_time:
|
||||||
|
qs = qs.filter(CreateTime__lte=end_time)
|
||||||
|
|
||||||
|
has_kaoheguan = request_data.get('has_kaoheguan')
|
||||||
|
if has_kaoheguan in (True, 'true', '1', 1):
|
||||||
|
qs = qs.exclude(shenheguan_id__isnull=True).exclude(shenheguan_id='')
|
||||||
|
elif has_kaoheguan in (False, 'false', '0', 0):
|
||||||
|
qs = qs.filter(Q(shenheguan_id__isnull=True) | Q(shenheguan_id=''))
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_shenhe_jilu_row(jl, profile_map, fee_map=None, reject_count_map=None):
|
||||||
|
dashou = profile_map.get(jl.shenqingren_id, {
|
||||||
|
'yonghuid': jl.shenqingren_id,
|
||||||
|
'avatar': '',
|
||||||
|
'phone': '',
|
||||||
|
'nicheng': jl.shenqingren_id,
|
||||||
|
})
|
||||||
|
kaoheguan = None
|
||||||
|
if jl.shenheguan_id:
|
||||||
|
kaoheguan = profile_map.get(jl.shenheguan_id, {
|
||||||
|
'yonghuid': jl.shenheguan_id,
|
||||||
|
'avatar': '',
|
||||||
|
'phone': '',
|
||||||
|
'nicheng': jl.shenheguan_id,
|
||||||
|
})
|
||||||
|
kaoheguan['game_id'] = jl.shenheguan_youxi_id or ''
|
||||||
|
|
||||||
|
current_fee = 0.0
|
||||||
|
if fee_map is not None:
|
||||||
|
current_fee = fee_map.get((jl.jilu_id, jl.cishu), 0.0)
|
||||||
|
else:
|
||||||
|
fd = KaoheJiluFeiyong.query.filter(jilu_id=jl.jilu_id, cishu=jl.cishu).first()
|
||||||
|
if fd:
|
||||||
|
current_fee = float(fd.jine)
|
||||||
|
|
||||||
|
reject_count = 0
|
||||||
|
if reject_count_map is not None:
|
||||||
|
reject_count = reject_count_map.get(jl.jilu_id, 0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'jilu_id': jl.jilu_id,
|
||||||
|
'zhuangtai': jl.zhuangtai,
|
||||||
|
'zhuangtai_text': SHENHE_ZHUANGTAI_MAP.get(jl.zhuangtai, str(jl.zhuangtai)),
|
||||||
|
'cishu': jl.cishu,
|
||||||
|
'jiaofei_jine': float(jl.jiaofei_jine or 0),
|
||||||
|
'current_fee': current_fee,
|
||||||
|
'bankuai_id': jl.bankuai.bankuai_id if jl.bankuai else None,
|
||||||
|
'bankuai_name': jl.bankuai.mingcheng if jl.bankuai else '',
|
||||||
|
'chenghao_id': jl.chenghao.id if jl.chenghao else None,
|
||||||
|
'chenghao_name': jl.chenghao.mingcheng if jl.chenghao else '',
|
||||||
|
'chenghao_icon': jl.chenghao.tubiao_url if jl.chenghao else '',
|
||||||
|
'guize_neirong': jl.guize_neirong or '',
|
||||||
|
'dashou': {
|
||||||
|
**dashou,
|
||||||
|
'game_id': jl.dashou_youxi_id or '',
|
||||||
|
'beizhu': jl.dashou_beizhu or '',
|
||||||
|
},
|
||||||
|
'kaoheguan': kaoheguan,
|
||||||
|
'reject_count': reject_count,
|
||||||
|
'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.CreateTime else '',
|
||||||
|
'UpdateTime': jl.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.UpdateTime else '',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class KhjlglView(APIView):
|
||||||
|
"""
|
||||||
|
考核记录列表 + 统计
|
||||||
|
POST /houtai/khjlgl
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
if 'kaohepeizhi' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
|
||||||
|
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('page_size', 20))
|
||||||
|
if page < 1:
|
||||||
|
page = 1
|
||||||
|
if page_size < 1:
|
||||||
|
page_size = 20
|
||||||
|
if page_size > 100:
|
||||||
|
page_size = 100
|
||||||
|
|
||||||
|
from jituan.services.shenhe_club import filter_shenhe_jilu_by_request
|
||||||
|
|
||||||
|
filter_data = request.data
|
||||||
|
qs = filter_shenhe_jilu_by_request(
|
||||||
|
_build_shenhe_jilu_filters(filter_data),
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
|
||||||
|
# FluentQuery.filter() 会原地修改同一对象;统计与列表必须各自独立查询
|
||||||
|
stats_base = filter_shenhe_jilu_by_request(
|
||||||
|
_build_shenhe_jilu_filters(filter_data),
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
agg = stats_base.aggregate(total_fee=Sum('jiaofei_jine'))
|
||||||
|
tag_rows = filter_shenhe_jilu_by_request(
|
||||||
|
_build_shenhe_jilu_filters(filter_data),
|
||||||
|
request,
|
||||||
|
).values(
|
||||||
|
'chenghao_id', 'chenghao__mingcheng'
|
||||||
|
).annotate(
|
||||||
|
total=Count('jilu_id'),
|
||||||
|
passed=Count('jilu_id', filter=Q(zhuangtai=1)),
|
||||||
|
failed=Count('jilu_id', filter=Q(zhuangtai=2)),
|
||||||
|
in_progress=Count('jilu_id', filter=Q(zhuangtai=0)),
|
||||||
|
pending=Count('jilu_id', filter=Q(zhuangtai=3)),
|
||||||
|
).order_by('-total')
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
'total': stats_base.count(),
|
||||||
|
'passed': stats_base.filter(zhuangtai=1).count(),
|
||||||
|
'failed': stats_base.filter(zhuangtai=2).count(),
|
||||||
|
'in_progress': stats_base.filter(zhuangtai=0).count(),
|
||||||
|
'pending': stats_base.filter(zhuangtai=3).count(),
|
||||||
|
'total_fee': float(agg['total_fee'] or 0),
|
||||||
|
'tag_stats': [
|
||||||
|
{
|
||||||
|
'chenghao_id': r['chenghao_id'],
|
||||||
|
'chenghao_name': r['chenghao__mingcheng'] or '未知标签',
|
||||||
|
'total': r['total'],
|
||||||
|
'passed': r['passed'],
|
||||||
|
'failed': r['failed'],
|
||||||
|
'in_progress': r['in_progress'],
|
||||||
|
'pending': r['pending'],
|
||||||
|
}
|
||||||
|
for r in tag_rows if r['chenghao_id']
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
total = qs.count()
|
||||||
|
start = (page - 1) * page_size
|
||||||
|
rows = qs.order_by('-CreateTime')[start:start + page_size].to_list()
|
||||||
|
|
||||||
|
uids = []
|
||||||
|
jilu_ids = []
|
||||||
|
for jl in rows:
|
||||||
|
uids.append(jl.shenqingren_id)
|
||||||
|
if jl.shenheguan_id:
|
||||||
|
uids.append(jl.shenheguan_id)
|
||||||
|
jilu_ids.append(jl.jilu_id)
|
||||||
|
profile_map = _batch_user_profile_map(uids)
|
||||||
|
|
||||||
|
fee_pairs = [(jl.jilu_id, jl.cishu) for jl in rows]
|
||||||
|
fee_map = {}
|
||||||
|
if fee_pairs:
|
||||||
|
jids = [p[0] for p in fee_pairs]
|
||||||
|
for fd in KaoheJiluFeiyong.query.filter(jilu_id__in=jids):
|
||||||
|
fee_map[(fd.jilu_id, fd.cishu)] = float(fd.jine)
|
||||||
|
|
||||||
|
reject_counts = {
|
||||||
|
r['jilu_id']: r['cnt']
|
||||||
|
for r in KaoheJujueJilu.query.filter(jilu_id__in=jilu_ids)
|
||||||
|
.values('jilu_id')
|
||||||
|
.annotate(cnt=Count('id'))
|
||||||
|
.all()
|
||||||
|
} if jilu_ids else {}
|
||||||
|
|
||||||
|
data_list = [
|
||||||
|
_serialize_shenhe_jilu_row(jl, profile_map, fee_map, reject_counts)
|
||||||
|
for jl in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
|
||||||
|
all_tags = list(
|
||||||
|
Chenghao.query.filter(leixing='dashou')
|
||||||
|
.values('id', 'mingcheng', 'bankuai_id')
|
||||||
|
.order_by('mingcheng')
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'ok',
|
||||||
|
'data': {
|
||||||
|
'total': total,
|
||||||
|
'page': page,
|
||||||
|
'page_size': page_size,
|
||||||
|
'list': data_list,
|
||||||
|
'stats': stats,
|
||||||
|
'all_bankuai': all_bankuai,
|
||||||
|
'all_tags': all_tags,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception('KhjlglView 接口错误')
|
||||||
|
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class KhjlczView(APIView):
|
||||||
|
"""
|
||||||
|
考核记录操作 / 详情
|
||||||
|
POST /houtai/khjlcz
|
||||||
|
action:
|
||||||
|
- get_detail: 获取单条详情(含缴费明细、拒绝历史)
|
||||||
|
- to_pending: 考核中 → 待审核(取消指定考核官,与小程序 transfer 一致)
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
if 'kaohepeizhi' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
|
||||||
|
|
||||||
|
action = request.data.get('action')
|
||||||
|
jilu_id = (request.data.get('jilu_id') or '').strip()
|
||||||
|
if not jilu_id:
|
||||||
|
return Response({'code': 1, 'msg': '缺少记录ID'}, status=400)
|
||||||
|
|
||||||
|
if action == 'get_detail':
|
||||||
|
try:
|
||||||
|
jl = ShenheJilu.query.select_related('bankuai', 'chenghao').get(jilu_id=jilu_id)
|
||||||
|
except ShenheJilu.DoesNotExist:
|
||||||
|
return Response({'code': 1, 'msg': '记录不存在'}, status=404)
|
||||||
|
|
||||||
|
uids = [jl.shenqingren_id]
|
||||||
|
if jl.shenheguan_id:
|
||||||
|
uids.append(jl.shenheguan_id)
|
||||||
|
profile_map = _batch_user_profile_map(uids)
|
||||||
|
|
||||||
|
fee_list = [
|
||||||
|
{
|
||||||
|
'cishu': f.cishu,
|
||||||
|
'jine': float(f.jine),
|
||||||
|
}
|
||||||
|
for f in KaoheJiluFeiyong.query.filter(jilu_id=jilu_id).order_by('cishu')
|
||||||
|
]
|
||||||
|
reject_list = []
|
||||||
|
for r in KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'):
|
||||||
|
kg_prof = _batch_user_profile_map([r.shenheguan_id]).get(r.shenheguan_id, {})
|
||||||
|
reject_list.append({
|
||||||
|
'cishu': r.cishu,
|
||||||
|
'shenheguan_id': r.shenheguan_id,
|
||||||
|
'shenheguan_nicheng': kg_prof.get('nicheng', r.shenheguan_id),
|
||||||
|
'yuanyin': r.yuanyin,
|
||||||
|
'CreateTime': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||||||
|
})
|
||||||
|
|
||||||
|
record = _serialize_shenhe_jilu_row(jl, profile_map)
|
||||||
|
record['fee_list'] = fee_list
|
||||||
|
record['reject_list'] = reject_list
|
||||||
|
return Response({'code': 0, 'msg': 'ok', 'data': record})
|
||||||
|
|
||||||
|
if action == 'to_pending':
|
||||||
|
with transaction.atomic():
|
||||||
|
jl = ShenheJilu.objects.select_for_update().filter(jilu_id=jilu_id).first()
|
||||||
|
if not jl:
|
||||||
|
return Response({'code': 1, 'msg': '记录不存在'}, status=404)
|
||||||
|
if jl.zhuangtai != 0:
|
||||||
|
return Response({'code': 1, 'msg': '仅「考核中」的记录可退回待审核'}, status=400)
|
||||||
|
|
||||||
|
old_kg_id = jl.shenheguan_id
|
||||||
|
jl.shenheguan_id = None
|
||||||
|
jl.shenheguan_youxi_id = None
|
||||||
|
jl.zhuangtai = 3
|
||||||
|
jl.save(update_fields=['shenheguan_id', 'shenheguan_youxi_id', 'zhuangtai', 'UpdateTime'])
|
||||||
|
|
||||||
|
if old_kg_id:
|
||||||
|
UserShenheguan.query.filter(user__UserUID=old_kg_id).update(shenhe_zhuangtai=0)
|
||||||
|
|
||||||
|
return Response({'code': 0, 'msg': '已退回待审核,考核官指定已取消'})
|
||||||
|
|
||||||
|
return Response({'code': 1, 'msg': '无效的操作类型'}, status=400)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception('KhjlczView 接口错误')
|
||||||
|
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||||
|
|
||||||
968
backend/views/finance.py
Normal file
968
backend/views/finance.py
Normal file
@@ -0,0 +1,968 @@
|
|||||||
|
import hmac
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import xmltodict
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from calendar import monthrange
|
||||||
|
from decimal import Decimal
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# 工具类导入
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action,
|
||||||
|
update_shangjia_daily
|
||||||
|
)
|
||||||
|
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||||
|
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||||
|
from jituan.services.club_penalty import (
|
||||||
|
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||||
|
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||||
|
)
|
||||||
|
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
from ..utils import (
|
||||||
|
verify_kefu_permission,
|
||||||
|
PERMISSION_TO_SHENFEN,
|
||||||
|
SHENFEN_PROFILE_MAP,
|
||||||
|
has_fadan_view_permission,
|
||||||
|
has_merchant_order_permission,
|
||||||
|
check_fadan_permission,
|
||||||
|
pick_order_id,
|
||||||
|
pick_penalty_create_params,
|
||||||
|
write_xiugai_log,
|
||||||
|
XIUGAI_LEIXING_DASHOU,
|
||||||
|
XIUGAI_LEIXING_GUANSHI,
|
||||||
|
XIUGAI_LEIXING_SHANGJIA,
|
||||||
|
XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
XIUGAI_LEIXING_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# models 集中导入
|
||||||
|
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||||
|
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||||
|
## backend
|
||||||
|
from backend.models import WithdrawalDailyStats
|
||||||
|
|
||||||
|
## users
|
||||||
|
from users.models import (
|
||||||
|
UserGuanshi, UserBoss, UserZuzhang,
|
||||||
|
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
## orders
|
||||||
|
from orders.models import (
|
||||||
|
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||||
|
OrderPlayerHistory, Order, RefundRecord,
|
||||||
|
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||||
|
)
|
||||||
|
|
||||||
|
## shop
|
||||||
|
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||||
|
|
||||||
|
## products
|
||||||
|
from products.models import (
|
||||||
|
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||||
|
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||||
|
)
|
||||||
|
|
||||||
|
## config
|
||||||
|
from config.models import (
|
||||||
|
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||||
|
TixianQuotaDefault,
|
||||||
|
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||||
|
)
|
||||||
|
|
||||||
|
## rank
|
||||||
|
from rank.models import (
|
||||||
|
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||||
|
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||||
|
)
|
||||||
|
|
||||||
|
# 序列化器
|
||||||
|
from ..serializers import PopupPageSerializer
|
||||||
|
|
||||||
|
# 全局常量、日志对象
|
||||||
|
logger = logging.getLogger('houtai')
|
||||||
|
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||||
|
|
||||||
|
class CaiwuView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
if 'caiwu' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
|
||||||
|
|
||||||
|
from jituan.services.caiwu_stats import build_caiwu_payload
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = build_caiwu_payload(request)
|
||||||
|
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||||
|
except Exception:
|
||||||
|
logger.exception('CaiwuView 异常')
|
||||||
|
return Response({'code': 99, 'msg': '统计失败'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class CwhybkhqView(APIView):
|
||||||
|
"""
|
||||||
|
获取所有板块及对应会员列表
|
||||||
|
POST /houtai/cwhybkhq
|
||||||
|
Body: { "phone": "管理员手机号" }
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
if 'huiyuancaiwu' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
|
||||||
|
|
||||||
|
bankuai_list = []
|
||||||
|
for bk in Bankuai.query.all():
|
||||||
|
members = Huiyuan.query.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id')
|
||||||
|
bankuai_list.append({
|
||||||
|
'bankuai_id': bk.bankuai_id,
|
||||||
|
'mingcheng': bk.mingcheng,
|
||||||
|
'members': list(members)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("CwhybkhqView 接口错误")
|
||||||
|
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class HybkjtsjView(APIView):
|
||||||
|
"""
|
||||||
|
会员详细统计数据
|
||||||
|
POST /houtai/hybkjtsj
|
||||||
|
Body: {
|
||||||
|
"phone": "管理员手机号",
|
||||||
|
"huiyuan_id": "会员ID",
|
||||||
|
"granularity": "day" | "month" | "year",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 5,
|
||||||
|
"include_guanshi": true,
|
||||||
|
"include_zuzhang": true,
|
||||||
|
"summary_date": "2026-05-16"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
if 'huiyuancaiwu' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
|
||||||
|
|
||||||
|
huiyuan_id = request.data.get('huiyuan_id')
|
||||||
|
granularity = request.data.get('granularity', 'day')
|
||||||
|
year = request.data.get('year')
|
||||||
|
month = request.data.get('month')
|
||||||
|
include_guanshi = request.data.get('include_guanshi', False)
|
||||||
|
include_zuzhang = request.data.get('include_zuzhang', False)
|
||||||
|
summary_date = request.data.get('summary_date')
|
||||||
|
|
||||||
|
if not huiyuan_id:
|
||||||
|
return Response({'code': 1, 'msg': '缺少会员ID'}, status=400)
|
||||||
|
|
||||||
|
# 🔧 直接使用 date.today() 避免模块名冲突
|
||||||
|
now = date.today()
|
||||||
|
|
||||||
|
# ---------- 构造时间范围 ----------
|
||||||
|
if granularity == 'day':
|
||||||
|
if not year or not month:
|
||||||
|
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||||||
|
start_date = date(year, month, 1)
|
||||||
|
if month == 12:
|
||||||
|
end_date = date(year + 1, 1, 1)
|
||||||
|
else:
|
||||||
|
end_date = date(year, month + 1, 1)
|
||||||
|
trunc_func = TruncDate('CreateTime')
|
||||||
|
if not summary_date:
|
||||||
|
summary_date = now.strftime('%Y-%m-%d')
|
||||||
|
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
||||||
|
elif granularity == 'month':
|
||||||
|
if not year:
|
||||||
|
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||||||
|
start_date = date(year, 1, 1)
|
||||||
|
end_date = date(year + 1, 1, 1)
|
||||||
|
trunc_func = TruncMonth('CreateTime')
|
||||||
|
if not summary_date:
|
||||||
|
summary_date = now.strftime('%Y-%m')
|
||||||
|
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
||||||
|
elif granularity == 'year':
|
||||||
|
year = year or now.year
|
||||||
|
start_date = date(year, 1, 1)
|
||||||
|
end_date = date(year + 1, 1, 1)
|
||||||
|
trunc_func = TruncYear('CreateTime')
|
||||||
|
if not summary_date:
|
||||||
|
summary_date = str(year)
|
||||||
|
summary_q = date(year, 1, 1)
|
||||||
|
else:
|
||||||
|
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||||||
|
|
||||||
|
# 生成 naive datetime(避免 USE_TZ=False 报错)
|
||||||
|
start_dt = datetime.combine(start_date, datetime.min.time())
|
||||||
|
end_dt = datetime.combine(end_date, datetime.min.time())
|
||||||
|
|
||||||
|
# ---------- 充值过滤 ----------
|
||||||
|
chongzhi_filter = {
|
||||||
|
'huiyuan_id': huiyuan_id, # 会员ID筛选
|
||||||
|
'leixing': 1,
|
||||||
|
'zhuangtai': 3,
|
||||||
|
'CreateTime__gte': start_dt,
|
||||||
|
'CreateTime__lt': end_dt
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 分红过滤 ----------
|
||||||
|
fenhong_filter = {
|
||||||
|
'huiyuan_id': huiyuan_id, # 分红表会员ID筛选
|
||||||
|
'CreateTime__gte': start_dt,
|
||||||
|
'CreateTime__lt': end_dt
|
||||||
|
}
|
||||||
|
|
||||||
|
# 充值分组查询
|
||||||
|
chongzhi_qs = filter_queryset_by_club(
|
||||||
|
Czjilu.query.filter(**chongzhi_filter), request,
|
||||||
|
) \
|
||||||
|
.annotate(period=trunc_func) \
|
||||||
|
.values('period') \
|
||||||
|
.annotate(
|
||||||
|
chongzhi_count=Count('id'),
|
||||||
|
chongzhi_amount=Sum('jine')
|
||||||
|
).order_by('period')
|
||||||
|
|
||||||
|
# 分红分组查询
|
||||||
|
fenhong_qs = None
|
||||||
|
if include_guanshi or include_zuzhang:
|
||||||
|
fenhong_agg = {}
|
||||||
|
if include_guanshi:
|
||||||
|
fenhong_agg['guanshi_amount'] = Sum('fenhong')
|
||||||
|
if include_zuzhang:
|
||||||
|
fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong')
|
||||||
|
fenhong_qs = filter_gsfenhong_qs(
|
||||||
|
Gsfenhong.query.filter(**fenhong_filter), request,
|
||||||
|
) \
|
||||||
|
.annotate(period=trunc_func) \
|
||||||
|
.values('period') \
|
||||||
|
.annotate(**fenhong_agg) \
|
||||||
|
.order_by('period')
|
||||||
|
|
||||||
|
# 合并数据
|
||||||
|
data_map = {}
|
||||||
|
for item in chongzhi_qs:
|
||||||
|
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||||
|
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||||
|
item['period'].strftime('%Y')
|
||||||
|
data_map[period_str] = {
|
||||||
|
'chongzhi_count': item['chongzhi_count'],
|
||||||
|
'chongzhi_amount': float(item['chongzhi_amount'] or 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fenhong_qs:
|
||||||
|
for item in fenhong_qs:
|
||||||
|
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||||
|
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||||
|
item['period'].strftime('%Y')
|
||||||
|
if period_str not in data_map:
|
||||||
|
data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0}
|
||||||
|
if include_guanshi:
|
||||||
|
data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0)
|
||||||
|
if include_zuzhang:
|
||||||
|
data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0)
|
||||||
|
|
||||||
|
time_series = []
|
||||||
|
for period_str, vals in data_map.items():
|
||||||
|
guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0
|
||||||
|
zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0
|
||||||
|
vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2)
|
||||||
|
vals['date'] = period_str
|
||||||
|
time_series.append(vals)
|
||||||
|
time_series.sort(key=lambda x: x['date'])
|
||||||
|
|
||||||
|
# ---------- 汇总数据 ----------
|
||||||
|
summary_chongzhi_filter = chongzhi_filter.copy()
|
||||||
|
if granularity == 'day':
|
||||||
|
summary_chongzhi_filter['CreateTime__date'] = summary_q
|
||||||
|
elif granularity == 'month':
|
||||||
|
summary_chongzhi_filter['CreateTime__year'] = summary_q.year
|
||||||
|
summary_chongzhi_filter['CreateTime__month'] = summary_q.month
|
||||||
|
else:
|
||||||
|
summary_chongzhi_filter['CreateTime__year'] = summary_q.year
|
||||||
|
|
||||||
|
summary_chongzhi = Czjilu.query.filter(**summary_chongzhi_filter).aggregate(
|
||||||
|
total_count=Count('id'),
|
||||||
|
total_amount=Sum('jine')
|
||||||
|
)
|
||||||
|
total_chongzhi_count = summary_chongzhi['total_count'] or 0
|
||||||
|
total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0)
|
||||||
|
|
||||||
|
total_guanshi = 0.0
|
||||||
|
total_zuzhang = 0.0
|
||||||
|
if include_guanshi or include_zuzhang:
|
||||||
|
summary_fenhong_filter = fenhong_filter.copy()
|
||||||
|
if granularity == 'day':
|
||||||
|
summary_fenhong_filter['CreateTime__date'] = summary_q
|
||||||
|
elif granularity == 'month':
|
||||||
|
summary_fenhong_filter['CreateTime__year'] = summary_q.year
|
||||||
|
summary_fenhong_filter['CreateTime__month'] = summary_q.month
|
||||||
|
else:
|
||||||
|
summary_fenhong_filter['CreateTime__year'] = summary_q.year
|
||||||
|
|
||||||
|
fenhong_agg = {}
|
||||||
|
if include_guanshi:
|
||||||
|
fenhong_agg['guanshi_sum'] = Sum('fenhong')
|
||||||
|
if include_zuzhang:
|
||||||
|
fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong')
|
||||||
|
summary_fenhong = Gsfenhong.query.filter(**summary_fenhong_filter).aggregate(**fenhong_agg)
|
||||||
|
total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0)
|
||||||
|
total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0)
|
||||||
|
|
||||||
|
total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'ok',
|
||||||
|
'data': {
|
||||||
|
'time_series': time_series,
|
||||||
|
'summary': {
|
||||||
|
'total_chongzhi_count': total_chongzhi_count,
|
||||||
|
'total_chongzhi_amount': total_chongzhi_amount,
|
||||||
|
'total_guanshi_fenhong': total_guanshi,
|
||||||
|
'total_zuzhang_fenhong': total_zuzhang,
|
||||||
|
'total_shouyi': total_shouyi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("HybkjtsjView 接口错误")
|
||||||
|
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SzxxView(APIView):
|
||||||
|
"""
|
||||||
|
每日收支详细统计数据
|
||||||
|
POST /houtai/szxx
|
||||||
|
只读 daily_income_stat / daily_payout_stat,按 date 字段汇总。
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
from jituan.services.szxx_stats import build_szxx_payload
|
||||||
|
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
if 'caiwu' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
|
||||||
|
|
||||||
|
granularity = request.data.get('granularity', 'day')
|
||||||
|
year = request.data.get('year')
|
||||||
|
month = request.data.get('month')
|
||||||
|
summary_date = request.data.get('summary_date')
|
||||||
|
|
||||||
|
data, err = build_szxx_payload(request, granularity, year, month, summary_date)
|
||||||
|
if err:
|
||||||
|
return Response({'code': 1, 'msg': err}, status=400)
|
||||||
|
|
||||||
|
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("SzxxView 接口错误")
|
||||||
|
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CwddhqlxView(APIView):
|
||||||
|
"""
|
||||||
|
获取订单类型(商品类型)用于筛选
|
||||||
|
POST /houtai/cwddhqlx
|
||||||
|
Body: { "phone": "管理员手机号" }
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
if 'dingdancaiwu' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
|
||||||
|
|
||||||
|
# 获取非审核状态(shenhezhuangtai=1)的商品类型,只返回ID和名称
|
||||||
|
types = ShangpinLeixing.query.filter(shenhezhuangtai=1).values('id', 'jieshao')
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'ok',
|
||||||
|
'data': list(types)
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("CwddhqlxView 接口错误")
|
||||||
|
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CwhqjtddsjView(APIView):
|
||||||
|
"""
|
||||||
|
订单详细统计数据(修正版)
|
||||||
|
POST /houtai/cwhqjtddsj
|
||||||
|
Body: {
|
||||||
|
"phone": "管理员手机号",
|
||||||
|
"granularity": "day" | "month" | "year",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 5,
|
||||||
|
"type_id": 1, # 商品类型ID,0表示全部
|
||||||
|
"order_source": 1, # 1=平台单,2=商家单,0=全部
|
||||||
|
"summary_date": "2026-05-16"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
if 'dingdancaiwu' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
|
||||||
|
|
||||||
|
granularity = request.data.get('granularity', 'day')
|
||||||
|
year = request.data.get('year')
|
||||||
|
month = request.data.get('month')
|
||||||
|
type_id = request.data.get('type_id', 0)
|
||||||
|
order_source = request.data.get('order_source', 0)
|
||||||
|
summary_date = request.data.get('summary_date')
|
||||||
|
|
||||||
|
now = date.today()
|
||||||
|
|
||||||
|
# ---------- 时间范围 ----------
|
||||||
|
if granularity == 'day':
|
||||||
|
if not year or not month:
|
||||||
|
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||||||
|
start_date = date(year, month, 1)
|
||||||
|
end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
|
||||||
|
trunc_func = TruncDate('CreateTime')
|
||||||
|
if not summary_date:
|
||||||
|
summary_date = now.strftime('%Y-%m-%d')
|
||||||
|
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
||||||
|
elif granularity == 'month':
|
||||||
|
if not year:
|
||||||
|
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||||||
|
start_date = date(year, 1, 1)
|
||||||
|
end_date = date(year + 1, 1, 1)
|
||||||
|
trunc_func = TruncMonth('CreateTime')
|
||||||
|
if not summary_date:
|
||||||
|
summary_date = now.strftime('%Y-%m')
|
||||||
|
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
||||||
|
elif granularity == 'year':
|
||||||
|
year = year or now.year
|
||||||
|
start_date = date(year, 1, 1)
|
||||||
|
end_date = date(year + 1, 1, 1)
|
||||||
|
trunc_func = TruncYear('CreateTime')
|
||||||
|
if not summary_date:
|
||||||
|
summary_date = str(year)
|
||||||
|
summary_q = date(year, 1, 1)
|
||||||
|
else:
|
||||||
|
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||||||
|
|
||||||
|
start_dt = datetime.combine(start_date, datetime.min.time())
|
||||||
|
end_dt = datetime.combine(end_date, datetime.min.time())
|
||||||
|
|
||||||
|
# ---------- 基础过滤 ----------
|
||||||
|
filters = {
|
||||||
|
'CreateTime__gte': start_dt,
|
||||||
|
'CreateTime__lt': end_dt,
|
||||||
|
'Status__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态
|
||||||
|
}
|
||||||
|
if int(type_id) != 0:
|
||||||
|
filters['ProductTypeID'] = type_id
|
||||||
|
if int(order_source) in [1, 2]:
|
||||||
|
filters['Platform'] = order_source
|
||||||
|
|
||||||
|
# ---------- 时间序列分组查询 ----------
|
||||||
|
order_base = filter_queryset_by_club(
|
||||||
|
Order.query.filter(**filters), request, club_field='ClubID',
|
||||||
|
)
|
||||||
|
qs = order_base \
|
||||||
|
.annotate(period=trunc_func) \
|
||||||
|
.values('period') \
|
||||||
|
.annotate(
|
||||||
|
# 订单总量(所有符合条件的状态)
|
||||||
|
order_count=Count('id'),
|
||||||
|
# 订单总额
|
||||||
|
order_amount=Sum('Amount'),
|
||||||
|
# 成交单量 (Status=3)
|
||||||
|
success_count=Count('id', filter=Q(Status=3)),
|
||||||
|
# 成交订单总额
|
||||||
|
success_amount=Sum('Amount', filter=Q(Status=3)),
|
||||||
|
# 退款单量 (Status=5)
|
||||||
|
refund_count=Count('id', filter=Q(Status=5)),
|
||||||
|
# 打手分成总额(仅成交订单)
|
||||||
|
PlayerCommission_sum=Sum('PlayerCommission', filter=Q(Status=3)),
|
||||||
|
# 平台成交订单的店铺分红
|
||||||
|
dianpu_fenhong_sum=Sum('pingtai_kuozhan__ShopIncome',
|
||||||
|
filter=Q(Status=3, Platform=1)),
|
||||||
|
# 成交订单中:平台单金额、商家单金额、各自打手分成
|
||||||
|
platform_success_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
|
||||||
|
merchant_success_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
|
||||||
|
platform_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
|
||||||
|
merchant_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
|
||||||
|
).order_by('period')
|
||||||
|
|
||||||
|
time_series = []
|
||||||
|
for item in qs:
|
||||||
|
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||||
|
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||||
|
item['period'].strftime('%Y')
|
||||||
|
|
||||||
|
# 平台收益计算
|
||||||
|
platform_profit = (item['platform_success_amount'] or 0) \
|
||||||
|
- (item['platform_dashou'] or 0) \
|
||||||
|
- (item['dianpu_fenhong_sum'] or 0)
|
||||||
|
merchant_profit = (item['merchant_success_amount'] or 0) \
|
||||||
|
- (item['merchant_dashou'] or 0)
|
||||||
|
total_profit = platform_profit + merchant_profit
|
||||||
|
|
||||||
|
time_series.append({
|
||||||
|
'date': period_str,
|
||||||
|
'order_count': item['order_count'],
|
||||||
|
'order_amount': float(item['order_amount'] or 0),
|
||||||
|
'success_count': item['success_count'],
|
||||||
|
'success_amount': float(item['success_amount'] or 0),
|
||||||
|
'refund_count': item['refund_count'],
|
||||||
|
'dashou_fencheng': float(item['PlayerCommission_sum'] or 0),
|
||||||
|
'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0),
|
||||||
|
'profit': round(total_profit, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
# ---------- 汇总数据 ----------
|
||||||
|
summary_qs = filter_queryset_by_club(
|
||||||
|
Order.query.filter(**filters), request, club_field='ClubID',
|
||||||
|
)
|
||||||
|
total_order_count = summary_qs.count()
|
||||||
|
total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0
|
||||||
|
total_success_count = summary_qs.filter(Status=3).count()
|
||||||
|
total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0
|
||||||
|
total_refund_count = summary_qs.filter(Status=5).count()
|
||||||
|
total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||||||
|
total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate(
|
||||||
|
s=Sum('pingtai_kuozhan__ShopIncome'))['s'] or 0
|
||||||
|
|
||||||
|
platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0
|
||||||
|
merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0
|
||||||
|
platform_dashou_sum = \
|
||||||
|
summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||||||
|
merchant_dashou_sum = \
|
||||||
|
summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||||||
|
|
||||||
|
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
|
||||||
|
profit_merchant = merchant_amount - merchant_dashou_sum
|
||||||
|
total_profit = profit_platform + profit_merchant
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
'total_order_count': total_order_count,
|
||||||
|
'total_order_amount': round(float(total_order_amount), 2),
|
||||||
|
'total_success_count': total_success_count,
|
||||||
|
'total_success_amount': round(float(total_success_amount), 2),
|
||||||
|
'total_refund_count': total_refund_count,
|
||||||
|
'total_PlayerCommission': round(float(total_dashou), 2),
|
||||||
|
'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2),
|
||||||
|
'total_profit': round(total_profit, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'ok',
|
||||||
|
'data': {
|
||||||
|
'time_series': time_series,
|
||||||
|
'summary': summary
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("CwhqjtddsjView 接口错误")
|
||||||
|
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CwqtczhqView(APIView):
|
||||||
|
"""
|
||||||
|
其他充值/罚款数据统计(非会员充值)
|
||||||
|
POST /houtai/cwqtczhq
|
||||||
|
Body: {
|
||||||
|
"phone": "管理员手机号",
|
||||||
|
"granularity": "day" | "month" | "year",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 5, // day时必填
|
||||||
|
"types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
|
||||||
|
"summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
if 'qtczcaiwu' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
|
||||||
|
|
||||||
|
from jituan.services.finance_detail_stats import build_chongzhi_finance_payload
|
||||||
|
|
||||||
|
granularity = request.data.get('granularity', 'day')
|
||||||
|
year = request.data.get('year')
|
||||||
|
month = request.data.get('month')
|
||||||
|
types = request.data.get('types', [2, 3, 4, 5, 6])
|
||||||
|
summary_date = request.data.get('summary_date')
|
||||||
|
|
||||||
|
if isinstance(types, list) and len(types) == 0:
|
||||||
|
types = [2, 3, 4, 5, 6]
|
||||||
|
|
||||||
|
data, err = build_chongzhi_finance_payload(
|
||||||
|
request, granularity, year, month, types, summary_date,
|
||||||
|
)
|
||||||
|
if err:
|
||||||
|
return Response({'code': 1, 'msg': err}, status=400)
|
||||||
|
|
||||||
|
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("CwqtczhqView 接口错误")
|
||||||
|
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||||
|
|
||||||
|
'''class CwqtczhqView_LEGACY(APIView):
|
||||||
|
"""
|
||||||
|
其他充值/罚款数据统计(非会员充值)
|
||||||
|
POST /houtai/cwqtczhq
|
||||||
|
Body: {
|
||||||
|
"phone": "管理员手机号",
|
||||||
|
"granularity": "day" | "month" | "year",
|
||||||
|
"year": 2026,
|
||||||
|
"month": 5, // day时必填
|
||||||
|
"types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
|
||||||
|
"summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
if 'qtczcaiwu' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
|
||||||
|
|
||||||
|
granularity = request.data.get('granularity', 'day')
|
||||||
|
year = request.data.get('year')
|
||||||
|
month = request.data.get('month')
|
||||||
|
types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部
|
||||||
|
summary_date = request.data.get('summary_date')
|
||||||
|
|
||||||
|
if isinstance(types, list) and len(types) == 0:
|
||||||
|
types = [2, 3, 4, 5, 6]
|
||||||
|
|
||||||
|
now = date.today()
|
||||||
|
|
||||||
|
# ---------- 时间范围(用于曲线图) ----------
|
||||||
|
if granularity == 'day':
|
||||||
|
if not year or not month:
|
||||||
|
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||||||
|
start_date = date(year, month, 1)
|
||||||
|
end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
|
||||||
|
trunc_func = TruncDate('CreateTime')
|
||||||
|
# 默认汇总日期
|
||||||
|
if not summary_date:
|
||||||
|
summary_date = now.strftime('%Y-%m-%d')
|
||||||
|
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
||||||
|
elif granularity == 'month':
|
||||||
|
if not year:
|
||||||
|
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||||||
|
start_date = date(year, 1, 1)
|
||||||
|
end_date = date(year + 1, 1, 1)
|
||||||
|
trunc_func = TruncMonth('CreateTime')
|
||||||
|
if not summary_date:
|
||||||
|
summary_date = now.strftime('%Y-%m')
|
||||||
|
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
||||||
|
elif granularity == 'year':
|
||||||
|
year = year or now.year
|
||||||
|
start_date = date(year, 1, 1)
|
||||||
|
end_date = date(year + 1, 1, 1)
|
||||||
|
trunc_func = TruncYear('CreateTime')
|
||||||
|
if not summary_date:
|
||||||
|
summary_date = str(year)
|
||||||
|
summary_q = date(year, 1, 1)
|
||||||
|
else:
|
||||||
|
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||||||
|
|
||||||
|
start_dt = datetime.combine(start_date, datetime.min.time())
|
||||||
|
end_dt = datetime.combine(end_date, datetime.min.time())
|
||||||
|
|
||||||
|
# ---------- 构建时间序列 ----------
|
||||||
|
time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} }
|
||||||
|
|
||||||
|
def ensure_date(ds):
|
||||||
|
if ds not in time_series_map:
|
||||||
|
time_series_map[ds] = {}
|
||||||
|
return time_series_map[ds]
|
||||||
|
|
||||||
|
# 1. 处理充值记录(Czjilu)
|
||||||
|
cz_types = [t for t in types if t in [2, 3, 4, 5]]
|
||||||
|
if cz_types:
|
||||||
|
cz_qs = Czjilu.query.filter(
|
||||||
|
CreateTime__gte=start_dt,
|
||||||
|
CreateTime__lt=end_dt,
|
||||||
|
leixing__in=cz_types,
|
||||||
|
zhuangtai=3 # 只统计支付成功的
|
||||||
|
).annotate(
|
||||||
|
period=trunc_func
|
||||||
|
).values('period', 'leixing').annotate(
|
||||||
|
total_count=Count('id'),
|
||||||
|
total_amount=Sum('jine')
|
||||||
|
).order_by('period', 'leixing')
|
||||||
|
|
||||||
|
for item in cz_qs:
|
||||||
|
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||||
|
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||||
|
item['period'].strftime('%Y')
|
||||||
|
lx = item['leixing']
|
||||||
|
amount = float(item['total_amount'] or 0)
|
||||||
|
count = item['total_count']
|
||||||
|
# 收益:积分(3)收益为金额本身,其他为0
|
||||||
|
profit = amount if lx == 3 else 0.0
|
||||||
|
|
||||||
|
day_data = ensure_date(period_str)
|
||||||
|
day_data[str(lx)] = {
|
||||||
|
'count': count,
|
||||||
|
'amount': amount,
|
||||||
|
'profit': profit,
|
||||||
|
'fenhong_count': 0, # 充值类型无分红
|
||||||
|
'fenhong_amount': 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. 处理罚款(Penalty)—— 包含分红
|
||||||
|
if 6 in types:
|
||||||
|
# 罚款记录
|
||||||
|
penalty_qs = Penalty.query.filter(
|
||||||
|
CreateTime__gte=start_dt,
|
||||||
|
CreateTime__lt=end_dt,
|
||||||
|
Status=2 # 已缴纳
|
||||||
|
).annotate(
|
||||||
|
period=trunc_func
|
||||||
|
).values('period').annotate(
|
||||||
|
penalty_count=Count('id'),
|
||||||
|
penalty_amount=Sum('FineAmount')
|
||||||
|
).order_by('period')
|
||||||
|
|
||||||
|
# 分红记录
|
||||||
|
fenhong_qs = PenaltyBonus.query.filter(
|
||||||
|
CreateTime__gte=start_dt,
|
||||||
|
CreateTime__lt=end_dt
|
||||||
|
).annotate(
|
||||||
|
period=trunc_func
|
||||||
|
).values('period').annotate(
|
||||||
|
fh_count=Count('id'),
|
||||||
|
fh_amount=Sum('WithdrawableAmount')
|
||||||
|
).order_by('period')
|
||||||
|
|
||||||
|
# 合并
|
||||||
|
for item in penalty_qs:
|
||||||
|
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||||
|
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||||
|
item['period'].strftime('%Y')
|
||||||
|
day_data = ensure_date(period_str)
|
||||||
|
day_data['6'] = {
|
||||||
|
'count': item['penalty_count'],
|
||||||
|
'amount': float(item['penalty_amount'] or 0),
|
||||||
|
'profit': 0.0, # 先设为0,后续计算
|
||||||
|
'fenhong_count': 0,
|
||||||
|
'fenhong_amount': 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
for item in fenhong_qs:
|
||||||
|
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||||
|
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||||
|
item['period'].strftime('%Y')
|
||||||
|
day_data = ensure_date(period_str)
|
||||||
|
if '6' not in day_data:
|
||||||
|
day_data['6'] = {'count': 0, 'amount': 0.0, 'profit': 0.0, 'fenhong_count': 0, 'fenhong_amount': 0.0}
|
||||||
|
day_data['6']['fenhong_count'] += item['fh_count']
|
||||||
|
day_data['6']['fenhong_amount'] += float(item['fh_amount'] or 0)
|
||||||
|
|
||||||
|
# 计算罚款收益 = 罚款金额 - 分红金额
|
||||||
|
for period_str, day_data in time_series_map.items():
|
||||||
|
if '6' in day_data:
|
||||||
|
penalty = day_data['6']
|
||||||
|
penalty['profit'] = round(penalty['amount'] - penalty['fenhong_amount'], 2)
|
||||||
|
|
||||||
|
# 构造有序时间序列列表
|
||||||
|
time_series = []
|
||||||
|
for period_str in sorted(time_series_map.keys()):
|
||||||
|
entry = {'date': period_str, 'types': {}}
|
||||||
|
for type_code in sorted(time_series_map[period_str].keys()):
|
||||||
|
entry['types'][type_code] = time_series_map[period_str][type_code]
|
||||||
|
time_series.append(entry)
|
||||||
|
|
||||||
|
# ---------- 汇总指定日期 ----------
|
||||||
|
summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
|
||||||
|
|
||||||
|
# 汇总充值类型
|
||||||
|
for lx in cz_types:
|
||||||
|
filter_kwargs = {
|
||||||
|
'leixing': lx,
|
||||||
|
'zhuangtai': 3,
|
||||||
|
}
|
||||||
|
if granularity == 'day':
|
||||||
|
filter_kwargs['CreateTime__date'] = summary_q
|
||||||
|
elif granularity == 'month':
|
||||||
|
filter_kwargs['CreateTime__year'] = summary_q.year
|
||||||
|
filter_kwargs['CreateTime__month'] = summary_q.month
|
||||||
|
else:
|
||||||
|
filter_kwargs['CreateTime__year'] = summary_q.year
|
||||||
|
agg = filter_queryset_by_club(
|
||||||
|
Czjilu.query.filter(**filter_kwargs), request,
|
||||||
|
).aggregate(count=Count('id'), amount=Sum('jine'))
|
||||||
|
count = agg['count'] or 0
|
||||||
|
amount = float(agg['amount'] or 0)
|
||||||
|
profit = amount if lx == 3 else 0.0
|
||||||
|
summary_result['by_type'][str(lx)] = {
|
||||||
|
'count': count,
|
||||||
|
'amount': round(amount, 2),
|
||||||
|
'profit': round(profit, 2),
|
||||||
|
'fenhong_count': 0,
|
||||||
|
'fenhong_amount': 0.0
|
||||||
|
}
|
||||||
|
summary_result['total_count'] += count
|
||||||
|
summary_result['total_amount'] += amount
|
||||||
|
summary_result['total_profit'] += profit
|
||||||
|
|
||||||
|
# 汇总罚款
|
||||||
|
if 6 in types:
|
||||||
|
penalty_filter = {
|
||||||
|
'Status': 2,
|
||||||
|
}
|
||||||
|
fenhong_filter = {}
|
||||||
|
if granularity == 'day':
|
||||||
|
penalty_filter['CreateTime__date'] = summary_q
|
||||||
|
fenhong_filter['CreateTime__date'] = summary_q
|
||||||
|
elif granularity == 'month':
|
||||||
|
penalty_filter['CreateTime__year'] = summary_q.year
|
||||||
|
penalty_filter['CreateTime__month'] = summary_q.month
|
||||||
|
fenhong_filter['CreateTime__year'] = summary_q.year
|
||||||
|
fenhong_filter['CreateTime__month'] = summary_q.month
|
||||||
|
else:
|
||||||
|
penalty_filter['CreateTime__year'] = summary_q.year
|
||||||
|
fenhong_filter['CreateTime__year'] = summary_q.year
|
||||||
|
|
||||||
|
penalty_agg = Penalty.query.filter(**penalty_filter).aggregate(
|
||||||
|
count=Count('id'), amount=Sum('FineAmount'))
|
||||||
|
penalty_count = penalty_agg['count'] or 0
|
||||||
|
penalty_amount = float(penalty_agg['amount'] or 0)
|
||||||
|
|
||||||
|
fenhong_agg = PenaltyBonus.query.filter(**fenhong_filter).aggregate(
|
||||||
|
fh_count=Count('id'), fh_amount=Sum('WithdrawableAmount'))
|
||||||
|
fh_count = fenhong_agg['fh_count'] or 0
|
||||||
|
fh_amount = float(fenhong_agg['fh_amount'] or 0)
|
||||||
|
|
||||||
|
profit = round(penalty_amount - fh_amount, 2)
|
||||||
|
summary_result['by_type']['6'] = {
|
||||||
|
'count': penalty_count,
|
||||||
|
'amount': round(penalty_amount, 2),
|
||||||
|
'profit': profit,
|
||||||
|
'fenhong_count': fh_count,
|
||||||
|
'fenhong_amount': round(fh_amount, 2)
|
||||||
|
}
|
||||||
|
summary_result['total_count'] += penalty_count
|
||||||
|
summary_result['total_amount'] += penalty_amount
|
||||||
|
summary_result['total_profit'] += profit
|
||||||
|
|
||||||
|
summary_result['total_amount'] = round(summary_result['total_amount'], 2)
|
||||||
|
summary_result['total_profit'] = round(summary_result['total_profit'], 2)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'ok',
|
||||||
|
'data': {
|
||||||
|
'time_series': time_series,
|
||||||
|
'summary': summary_result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("CwqtczhqView 接口错误")
|
||||||
|
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)'''
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 后端接口 ====================
|
||||||
|
# 文件:views.py (添加以下两个视图类)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
817
backend/views/guanli.py
Normal file
817
backend/views/guanli.py
Normal file
@@ -0,0 +1,817 @@
|
|||||||
|
import hmac
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import xmltodict
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from calendar import monthrange
|
||||||
|
from decimal import Decimal
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# 工具类导入
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action,
|
||||||
|
update_shangjia_daily
|
||||||
|
)
|
||||||
|
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||||
|
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||||
|
from jituan.services.club_penalty import (
|
||||||
|
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||||
|
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||||
|
)
|
||||||
|
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
from ..utils import (
|
||||||
|
verify_kefu_permission,
|
||||||
|
PERMISSION_TO_SHENFEN,
|
||||||
|
SHENFEN_PROFILE_MAP,
|
||||||
|
has_fadan_view_permission,
|
||||||
|
has_merchant_order_permission,
|
||||||
|
check_fadan_permission,
|
||||||
|
pick_order_id,
|
||||||
|
pick_penalty_create_params,
|
||||||
|
write_xiugai_log,
|
||||||
|
XIUGAI_LEIXING_DASHOU,
|
||||||
|
XIUGAI_LEIXING_GUANSHI,
|
||||||
|
XIUGAI_LEIXING_SHANGJIA,
|
||||||
|
XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
XIUGAI_LEIXING_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# models 集中导入
|
||||||
|
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||||
|
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||||
|
## backend
|
||||||
|
from backend.models import WithdrawalDailyStats
|
||||||
|
|
||||||
|
## users
|
||||||
|
from users.models import (
|
||||||
|
UserGuanshi, UserBoss, UserZuzhang,
|
||||||
|
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
## orders
|
||||||
|
from orders.models import (
|
||||||
|
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||||
|
OrderPlayerHistory, Order, RefundRecord,
|
||||||
|
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||||
|
)
|
||||||
|
|
||||||
|
## shop
|
||||||
|
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||||
|
|
||||||
|
## products
|
||||||
|
from products.models import (
|
||||||
|
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||||
|
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||||
|
)
|
||||||
|
|
||||||
|
## config
|
||||||
|
from config.models import (
|
||||||
|
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||||
|
TixianQuotaDefault,
|
||||||
|
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||||
|
)
|
||||||
|
|
||||||
|
## rank
|
||||||
|
from rank.models import (
|
||||||
|
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||||
|
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||||
|
)
|
||||||
|
|
||||||
|
# 序列化器
|
||||||
|
from ..serializers import PopupPageSerializer
|
||||||
|
|
||||||
|
# 全局常量、日志对象
|
||||||
|
logger = logging.getLogger('houtai')
|
||||||
|
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||||
|
|
||||||
|
class GetGuanliListView(APIView):
|
||||||
|
"""
|
||||||
|
获取管事列表
|
||||||
|
权限:4400a,4400b,4400c,4400d,4400e,4400f 任一
|
||||||
|
POST /houtai/hthqgslb
|
||||||
|
参数:
|
||||||
|
username, keyword(可选), balance_op/balance_value, commission_op/commission_value,
|
||||||
|
invite_count_op/invite_count_value, status, page, page_size
|
||||||
|
返回:
|
||||||
|
code:0, data: { list: [...], total }
|
||||||
|
"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
|
||||||
|
# 权限校验(需要任一权限)
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
|
||||||
|
if not any(p in permissions for p in required_perms):
|
||||||
|
return Response({'code': 403, 'msg': '您没有权限查看管事列表'})
|
||||||
|
|
||||||
|
# 分页参数
|
||||||
|
try:
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('page_size', 20))
|
||||||
|
if page < 1 or page_size < 1:
|
||||||
|
raise ValueError
|
||||||
|
except:
|
||||||
|
return Response({'code': 400, 'msg': '分页参数错误'})
|
||||||
|
|
||||||
|
# 筛选参数
|
||||||
|
keyword = request.data.get('keyword', '').strip()
|
||||||
|
balance_op = request.data.get('balance_op', '').strip().lower()
|
||||||
|
balance_value = request.data.get('balance_value')
|
||||||
|
commission_op = request.data.get('commission_op', '').strip().lower()
|
||||||
|
commission_value = request.data.get('commission_value')
|
||||||
|
invite_count_op = request.data.get('invite_count_op', '').strip().lower()
|
||||||
|
invite_count_value = request.data.get('invite_count_value')
|
||||||
|
status = request.data.get('status')
|
||||||
|
|
||||||
|
# 构建查询:从 User 开始,关联 UserGuanshi 和 UserBoss(左连接获取昵称)
|
||||||
|
qs = filter_user_qs_by_club(
|
||||||
|
User.query.filter(
|
||||||
|
GuanshiProfile__isnull=False # 确保是管事
|
||||||
|
).select_related('GuanshiProfile').select_related('BossProfile'),
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 关键词搜索(用户ID 或 昵称来自 boss_profile.nickname)
|
||||||
|
if keyword:
|
||||||
|
qs = qs.filter(
|
||||||
|
Q(UserUID__icontains=keyword) |
|
||||||
|
Q(BossProfile__nickname__icontains=keyword)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 余额筛选(使用 guanshi_profile.yue)
|
||||||
|
if balance_op and balance_value is not None and balance_value != '':
|
||||||
|
try:
|
||||||
|
balance_val = Decimal(str(balance_value))
|
||||||
|
if balance_op == 'gte':
|
||||||
|
qs = qs.filter(GuanshiProfile__yue__gte=balance_val)
|
||||||
|
elif balance_op == 'lte':
|
||||||
|
qs = qs.filter(GuanshiProfile__yue__lte=balance_val)
|
||||||
|
else:
|
||||||
|
return Response({'code': 400, 'msg': '余额比较符无效'})
|
||||||
|
except:
|
||||||
|
return Response({'code': 400, 'msg': '余额数值格式错误'})
|
||||||
|
|
||||||
|
# 分佣总额筛选(guanshi_profile.chongzhifenrun)
|
||||||
|
if commission_op and commission_value is not None and commission_value != '':
|
||||||
|
try:
|
||||||
|
commission_val = Decimal(str(commission_value))
|
||||||
|
if commission_op == 'gte':
|
||||||
|
qs = qs.filter(GuanshiProfile__chongzhifenrun__gte=commission_val)
|
||||||
|
elif commission_op == 'lte':
|
||||||
|
qs = qs.filter(GuanshiProfile__chongzhifenrun__lte=commission_val)
|
||||||
|
else:
|
||||||
|
return Response({'code': 400, 'msg': '分佣比较符无效'})
|
||||||
|
except:
|
||||||
|
return Response({'code': 400, 'msg': '分佣数值格式错误'})
|
||||||
|
|
||||||
|
# 邀请打手总数筛选(guanshi_profile.yaogingshuliang)
|
||||||
|
if invite_count_op and invite_count_value is not None and invite_count_value != '':
|
||||||
|
try:
|
||||||
|
invite_val = int(invite_count_value)
|
||||||
|
if invite_count_op == 'gte':
|
||||||
|
qs = qs.filter(GuanshiProfile__yaogingshuliang__gte=invite_val)
|
||||||
|
elif invite_count_op == 'lte':
|
||||||
|
qs = qs.filter(GuanshiProfile__yaogingshuliang__lte=invite_val)
|
||||||
|
else:
|
||||||
|
return Response({'code': 400, 'msg': '邀请数量比较符无效'})
|
||||||
|
except:
|
||||||
|
return Response({'code': 400, 'msg': '邀请数量格式错误'})
|
||||||
|
|
||||||
|
# 状态筛选
|
||||||
|
if status is not None and status != '':
|
||||||
|
try:
|
||||||
|
status_val = int(status)
|
||||||
|
if status_val not in (0, 1):
|
||||||
|
raise ValueError
|
||||||
|
qs = qs.filter(GuanshiProfile__zhuangtai=status_val)
|
||||||
|
except:
|
||||||
|
return Response({'code': 400, 'msg': '状态值无效'})
|
||||||
|
|
||||||
|
# 计算总数
|
||||||
|
total = qs.count()
|
||||||
|
|
||||||
|
# 分页
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
users = qs.order_by('-UserCreateTime')[offset:offset + page_size]
|
||||||
|
|
||||||
|
# 构建返回数据
|
||||||
|
result = []
|
||||||
|
for user in users:
|
||||||
|
guanshi = user.GuanshiProfile
|
||||||
|
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||||||
|
result.append({
|
||||||
|
'yonghuid': user.UserUID,
|
||||||
|
'avatar': user.Avatar or '',
|
||||||
|
'nickname': boss.nickname if boss else '',
|
||||||
|
'zhuangtai': guanshi.zhuangtai,
|
||||||
|
'yaogingshuliang': guanshi.yaogingshuliang,
|
||||||
|
'yue': str(guanshi.yue),
|
||||||
|
'chongzhifenrun': str(guanshi.chongzhifenrun),
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
**list_response_meta(request),
|
||||||
|
'data': {'list': result, 'total': total},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class GetGuanliDetailView(APIView):
|
||||||
|
"""
|
||||||
|
获取管事详情
|
||||||
|
权限:4400a,4400b,4400c,4400d,4400e,4400f 任一
|
||||||
|
POST /houtai/hthqgsxq
|
||||||
|
参数:username, yonghuid
|
||||||
|
返回:
|
||||||
|
code:0, data: {
|
||||||
|
base: {...},
|
||||||
|
balance: {...},
|
||||||
|
permanent_enabled, permanent_amount,
|
||||||
|
custom_first_dividend: {},
|
||||||
|
extra_dividend_map: {},
|
||||||
|
all_members: []
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
yonghuid = request.data.get('yonghuid', '').strip()
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
if not yonghuid:
|
||||||
|
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||||||
|
|
||||||
|
# 权限校验(需要任一权限)
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
|
||||||
|
if not any(p in permissions for p in required_perms):
|
||||||
|
return Response({'code': 403, 'msg': '您没有权限查看管事详情'})
|
||||||
|
|
||||||
|
# 查询用户主表和管事扩展表
|
||||||
|
try:
|
||||||
|
user = User.query.select_related('GuanshiProfile', 'BossProfile').get(UserUID=yonghuid)
|
||||||
|
guanshi = user.GuanshiProfile
|
||||||
|
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '管事不存在'})
|
||||||
|
except UserGuanshi.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '用户不是管事'})
|
||||||
|
|
||||||
|
denied = forbid_if_user_out_of_scope(request, user)
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
|
|
||||||
|
# 基础信息
|
||||||
|
is_zuzhang = UserZuzhang.query.filter(user=user).exists()
|
||||||
|
base_data = {
|
||||||
|
'yonghuid': user.UserUID,
|
||||||
|
'nickname': boss.nickname if boss else '',
|
||||||
|
'avatar': user.Avatar or '',
|
||||||
|
'dianhua': guanshi.dianhua or '',
|
||||||
|
'wechat': guanshi.wechat or '',
|
||||||
|
'zhuangtai': guanshi.zhuangtai,
|
||||||
|
'yaoqingma': guanshi.yaoqingma,
|
||||||
|
'yaoqingren': guanshi.yaoqingren,
|
||||||
|
'invite_qrcode_url': guanshi.invite_qrcode_url or '',
|
||||||
|
'yaogingshuliang': guanshi.yaogingshuliang,
|
||||||
|
'jinrichongzhi': guanshi.jinrichongzhi,
|
||||||
|
'jinyuechongzhi': guanshi.jinyuechongzhi,
|
||||||
|
'chongzhifenrun': str(guanshi.chongzhifenrun),
|
||||||
|
'create_time': guanshi.CreateTime.isoformat() if guanshi.CreateTime else None,
|
||||||
|
'CreateTime': guanshi.CreateTime.isoformat() if guanshi.CreateTime else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 余额提现相关
|
||||||
|
balance_data = {
|
||||||
|
'yue': str(guanshi.yue),
|
||||||
|
'jinritixian_jine': str(guanshi.jinritixian_jine),
|
||||||
|
'last_tixian_date': guanshi.last_tixian_date.isoformat() if guanshi.last_tixian_date else None,
|
||||||
|
'kaioi_ewai_xiane': guanshi.kaioi_ewai_xiane,
|
||||||
|
'ewai_xiane': str(guanshi.ewai_xiane),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 永久分红(二次分红)
|
||||||
|
permanent_enabled = guanshi.fenghong_erci_enabled
|
||||||
|
permanent_amount = str(guanshi.fenghong_erci) if guanshi.fenghong_erci else '0'
|
||||||
|
|
||||||
|
# 获取所有会员(用于前端选择)
|
||||||
|
all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc'))
|
||||||
|
for m in all_members:
|
||||||
|
m['jiage'] = str(m['jiage'])
|
||||||
|
m['guanshifc'] = str(m['guanshifc'])
|
||||||
|
|
||||||
|
# 获取多次分红配置(按次数和会员组织,按俱乐部隔离)
|
||||||
|
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||||
|
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||||
|
duoci_records = DuociFenhong.query.filter(
|
||||||
|
yonghuid=yonghuid, club_id=guanshi_club,
|
||||||
|
).order_by('cishu')
|
||||||
|
custom_first = {} # 首次分红定制(cishu=1)
|
||||||
|
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } }
|
||||||
|
|
||||||
|
for record in duoci_records:
|
||||||
|
# 获取会员信息
|
||||||
|
member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first()
|
||||||
|
if not member:
|
||||||
|
continue
|
||||||
|
member_info = {
|
||||||
|
'jieshao': member.jieshao,
|
||||||
|
'jiage': str(member.jiage),
|
||||||
|
}
|
||||||
|
if record.cishu == 1:
|
||||||
|
custom_first[record.huiyuan] = float(record.guanshi_fenhong)
|
||||||
|
else:
|
||||||
|
times = record.cishu
|
||||||
|
if times not in extra_map:
|
||||||
|
extra_map[times] = {}
|
||||||
|
extra_map[times][record.huiyuan] = {
|
||||||
|
'guanshi_fenhong': float(record.guanshi_fenhong),
|
||||||
|
**member_info
|
||||||
|
}
|
||||||
|
|
||||||
|
# 注意:首次分红定制只存储有定制记录的会员,没有定制的会员使用默认值(前端通过会员表展示)
|
||||||
|
# 额外次数分红按次数返回
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'base': base_data,
|
||||||
|
'balance': balance_data,
|
||||||
|
'permanent_enabled': permanent_enabled,
|
||||||
|
'permanent_amount': permanent_amount,
|
||||||
|
'custom_first_dividend': custom_first,
|
||||||
|
'extra_dividend_map': extra_map,
|
||||||
|
'all_members': all_members,
|
||||||
|
'is_zuzhang': is_zuzhang,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_unique_zuzhang_yaoqingma(yonghuid):
|
||||||
|
"""生成唯一组长邀请码(与小程序组长注册逻辑一致)"""
|
||||||
|
timestamp = str(int(time.time()))[-6:]
|
||||||
|
uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0')
|
||||||
|
rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2))
|
||||||
|
base = f"Z{timestamp}{uid_part}{rand_str}"
|
||||||
|
for _ in range(5):
|
||||||
|
if not UserZuzhang.query.filter(yaoqingma=base).exists():
|
||||||
|
return base
|
||||||
|
base = base + random.choice(string.digits)
|
||||||
|
return base + str(int(time.time() * 1000))[-4:]
|
||||||
|
|
||||||
|
|
||||||
|
def _create_zuzhang_profile(user):
|
||||||
|
"""为已有用户创建组长扩展记录(默认字段与小程序注册一致)"""
|
||||||
|
invite_guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count()
|
||||||
|
yaoqingma = _generate_unique_zuzhang_yaoqingma(user.UserUID)
|
||||||
|
return UserZuzhang.query.create(
|
||||||
|
user=user,
|
||||||
|
yaoqingma=yaoqingma,
|
||||||
|
fenyong_zonge=Decimal('0.00'),
|
||||||
|
ketixian_jine=Decimal('0.00'),
|
||||||
|
yaoqing_zongshu=invite_guanshi_count,
|
||||||
|
jinri_fenyong=Decimal('0.00'),
|
||||||
|
jinyue_fenyong=Decimal('0.00'),
|
||||||
|
zhuangtai=1,
|
||||||
|
jinri_tixian=Decimal('0.00'),
|
||||||
|
ewai_tixian_xiane=Decimal('0.00'),
|
||||||
|
kaioi_ewai_tixian=False,
|
||||||
|
kaioi_ewai_fenhong=False,
|
||||||
|
ewai_fenhong_jine=Decimal('0.00'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PromoteGuanshiToZuzhangView(APIView):
|
||||||
|
"""
|
||||||
|
后台将管事用户升级为组长(写入 user_zuzhang 扩展表)
|
||||||
|
POST /houtai/htgssjzz
|
||||||
|
权限:sjzz666
|
||||||
|
参数:username, yonghuid
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
yonghuid = request.data.get('yonghuid', '').strip()
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
if not yonghuid:
|
||||||
|
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if 'sjzz666' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限升级组长(需要 sjzz666)'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = User.query.select_related('GuanshiProfile').get(UserUID=yonghuid)
|
||||||
|
guanshi = user.GuanshiProfile
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '用户不存在'})
|
||||||
|
except UserGuanshi.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '用户不是管事,无法升级组长'})
|
||||||
|
|
||||||
|
if UserZuzhang.query.filter(user=user).exists():
|
||||||
|
zuzhang = user.ZuzhangProfile
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'msg': '该用户已是组长',
|
||||||
|
'data': {
|
||||||
|
'yonghuid': user.UserUID,
|
||||||
|
'yaoqingma': zuzhang.yaoqingma,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if guanshi.zhuangtai != 1:
|
||||||
|
return Response({'code': 400, 'msg': '管事账号已禁用,无法升级组长'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
zuzhang = _create_zuzhang_profile(user)
|
||||||
|
except IntegrityError:
|
||||||
|
return Response({'code': 400, 'msg': '升级失败,请稍后重试'})
|
||||||
|
|
||||||
|
write_xiugai_log(
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
xiugaiid=kefu.user.Phone,
|
||||||
|
leixing=XIUGAI_LEIXING_GUANSHI,
|
||||||
|
qitashuoming=(
|
||||||
|
f'【后台】管事升级组长:用户ID={yonghuid},'
|
||||||
|
f'生成组长邀请码={zuzhang.yaoqingma},操作人={kefu.user.Phone}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"管事升级组长成功: yonghuid={yonghuid}, yaoqingma={zuzhang.yaoqingma}")
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': '已升级为组长',
|
||||||
|
'data': {
|
||||||
|
'yonghuid': user.UserUID,
|
||||||
|
'nickname': getattr(user.BossProfile, 'nickname', '') if hasattr(user, 'BossProfile') else '',
|
||||||
|
'yaoqingma': zuzhang.yaoqingma,
|
||||||
|
'yaoqing_zongshu': zuzhang.yaoqing_zongshu,
|
||||||
|
'ketixian_jine': str(zuzhang.ketixian_jine),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateGuanliView(APIView):
|
||||||
|
"""
|
||||||
|
修改管事信息(基础信息、余额、提现限额、分红策略)
|
||||||
|
权限细分:
|
||||||
|
- 修改状态(封禁/解封)及昵称/电话/微信:需要4400a
|
||||||
|
- 增加余额:需要4400c
|
||||||
|
- 减少余额:需要4400b
|
||||||
|
- 修改提现限额(kaioi_ewai_xiane/ewai_xiane):需要4400e
|
||||||
|
- 修改分红相关(永久分红、定制首次、额外次数):需要4400f
|
||||||
|
POST /houtai/htxggsxx
|
||||||
|
"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
yonghuid = request.data.get('yonghuid', '').strip()
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
if not yonghuid:
|
||||||
|
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||||||
|
|
||||||
|
# 权限校验(先获取权限列表)
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
# 查询管事对象(不在事务外加锁,事务内加锁)
|
||||||
|
try:
|
||||||
|
user = User.query.get(UserUID=yonghuid)
|
||||||
|
guanshi = user.GuanshiProfile
|
||||||
|
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '用户不存在'})
|
||||||
|
except UserGuanshi.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '用户不是管事'})
|
||||||
|
|
||||||
|
# 开始事务,内部使用 select_for_update
|
||||||
|
with transaction.atomic():
|
||||||
|
# 重新获取加锁对象
|
||||||
|
user = User.objects.select_for_update().get(UserUID=yonghuid)
|
||||||
|
guanshi = user.GuanshiProfile
|
||||||
|
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||||||
|
operator = kefu.user.Phone
|
||||||
|
|
||||||
|
# ---------- 1. 基础信息修改(需要4400a)----------
|
||||||
|
nickname = request.data.get('nickname')
|
||||||
|
dianhua = request.data.get('dianhua')
|
||||||
|
wechat = request.data.get('wechat')
|
||||||
|
zhuangtai = request.data.get('zhuangtai')
|
||||||
|
avatar = request.data.get('avatar')
|
||||||
|
|
||||||
|
if any(v is not None for v in [nickname, dianhua, wechat, zhuangtai, avatar]):
|
||||||
|
if '4400a' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改管事基础信息(需要4400a)'})
|
||||||
|
base_changes = []
|
||||||
|
old_nickname = boss.nickname if boss else ''
|
||||||
|
old_dianhua = guanshi.dianhua
|
||||||
|
old_wechat = guanshi.wechat
|
||||||
|
old_zhuangtai = guanshi.zhuangtai
|
||||||
|
# 处理昵称(在老板扩展表中)
|
||||||
|
if nickname is not None:
|
||||||
|
if not boss:
|
||||||
|
boss = UserBoss.query.create(user=user, nickname=nickname)
|
||||||
|
else:
|
||||||
|
boss.nickname = nickname
|
||||||
|
boss.save()
|
||||||
|
if str(nickname) != str(old_nickname):
|
||||||
|
base_changes.append(f'昵称:{old_nickname or "无"}→{nickname}')
|
||||||
|
# 处理其他字段
|
||||||
|
if dianhua is not None:
|
||||||
|
guanshi.dianhua = dianhua
|
||||||
|
if str(dianhua) != str(old_dianhua):
|
||||||
|
base_changes.append(f'电话:{old_dianhua or "无"}→{dianhua}')
|
||||||
|
if wechat is not None:
|
||||||
|
guanshi.wechat = wechat
|
||||||
|
if str(wechat) != str(old_wechat):
|
||||||
|
base_changes.append(f'微信:{old_wechat or "无"}→{wechat}')
|
||||||
|
if zhuangtai is not None:
|
||||||
|
try:
|
||||||
|
zt = int(zhuangtai)
|
||||||
|
if zt in (0, 1):
|
||||||
|
guanshi.zhuangtai = zt
|
||||||
|
if zt != old_zhuangtai:
|
||||||
|
zt_map = {0: '禁用', 1: '正常'}
|
||||||
|
base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}→{zt_map.get(zt, zt)}')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if avatar is not None:
|
||||||
|
user.Avatar = avatar
|
||||||
|
user.save()
|
||||||
|
base_changes.append('更新了头像')
|
||||||
|
guanshi.save()
|
||||||
|
if base_changes:
|
||||||
|
write_xiugai_log(
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
xiugaiid=operator,
|
||||||
|
leixing=XIUGAI_LEIXING_GUANSHI,
|
||||||
|
qitashuoming=(
|
||||||
|
f'【后台】修改管事基础信息:{";".join(base_changes)},'
|
||||||
|
f'管事ID={yonghuid},操作人={operator}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- 2. 余额修改(加:4400c;减:4400b)----------
|
||||||
|
new_yue = request.data.get('yue')
|
||||||
|
if new_yue is not None:
|
||||||
|
try:
|
||||||
|
new_yue = Decimal(str(new_yue))
|
||||||
|
if new_yue < 0:
|
||||||
|
return Response({'code': 400, 'msg': '余额不能为负数'})
|
||||||
|
except:
|
||||||
|
return Response({'code': 400, 'msg': '余额格式错误'})
|
||||||
|
|
||||||
|
old_yue = guanshi.yue
|
||||||
|
if new_yue > old_yue:
|
||||||
|
if '4400c' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限增加余额(需要4400c)'})
|
||||||
|
from jituan.services.group_finance_gate import deny_group_finance_exec
|
||||||
|
deny = deny_group_finance_exec(request.user, request)
|
||||||
|
if deny:
|
||||||
|
return deny
|
||||||
|
action_desc = f'增加{new_yue - old_yue}元'
|
||||||
|
elif new_yue < old_yue:
|
||||||
|
if '4400b' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限减少余额(需要4400b)'})
|
||||||
|
action_desc = f'减少{old_yue - new_yue}元'
|
||||||
|
else:
|
||||||
|
action_desc = '无变动'
|
||||||
|
if new_yue != old_yue:
|
||||||
|
guanshi.yue = new_yue
|
||||||
|
guanshi.save()
|
||||||
|
write_xiugai_log(
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
xiugaiid=operator,
|
||||||
|
leixing=XIUGAI_LEIXING_GUANSHI,
|
||||||
|
guanshiyue=new_yue,
|
||||||
|
guanshiyueq=old_yue,
|
||||||
|
qitashuoming=(
|
||||||
|
f'【后台】修改管事可提现余额:{old_yue}元 → {new_yue}元({action_desc}),'
|
||||||
|
f'管事ID={yonghuid},操作人={operator}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- 3. 提现限额修改(需要4400e)----------
|
||||||
|
kaioi_ewai = request.data.get('kaioi_ewai_xiane')
|
||||||
|
ewai_val = request.data.get('ewai_xiane')
|
||||||
|
if kaioi_ewai is not None or ewai_val is not None:
|
||||||
|
if '4400e' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改提现限额(需要4400e)'})
|
||||||
|
limit_changes = []
|
||||||
|
old_kaioi = guanshi.kaioi_ewai_xiane
|
||||||
|
old_ewai = guanshi.ewai_xiane
|
||||||
|
if kaioi_ewai is not None:
|
||||||
|
guanshi.kaioi_ewai_xiane = bool(kaioi_ewai)
|
||||||
|
if bool(kaioi_ewai) != old_kaioi:
|
||||||
|
limit_changes.append(f'额外限额开关:{"开" if old_kaioi else "关"}→{"开" if kaioi_ewai else "关"}')
|
||||||
|
if ewai_val is not None:
|
||||||
|
try:
|
||||||
|
guanshi.ewai_xiane = Decimal(str(ewai_val))
|
||||||
|
if guanshi.ewai_xiane != old_ewai:
|
||||||
|
limit_changes.append(f'额外限额金额:{old_ewai}→{guanshi.ewai_xiane}')
|
||||||
|
except:
|
||||||
|
return Response({'code': 400, 'msg': '额外限额金额格式错误'})
|
||||||
|
guanshi.save()
|
||||||
|
if limit_changes:
|
||||||
|
write_xiugai_log(
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
xiugaiid=operator,
|
||||||
|
leixing=XIUGAI_LEIXING_GUANSHI,
|
||||||
|
qitashuoming=(
|
||||||
|
f'【后台】修改管事提现限额:{";".join(limit_changes)},'
|
||||||
|
f'管事ID={yonghuid},操作人={operator}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- 4. 分红相关修改(需要4400f)----------
|
||||||
|
perm_enabled = request.data.get('permanent_enabled')
|
||||||
|
perm_amount = request.data.get('permanent_amount')
|
||||||
|
custom_first = request.data.get('custom_first_dividend')
|
||||||
|
extra_map = request.data.get('extra_dividend_map')
|
||||||
|
|
||||||
|
if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
|
||||||
|
if '4400f' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改分红策略(需要4400f)'})
|
||||||
|
|
||||||
|
# 4.1 永久分红
|
||||||
|
if perm_enabled is not None:
|
||||||
|
guanshi.fenghong_erci_enabled = bool(perm_enabled)
|
||||||
|
if perm_amount is not None:
|
||||||
|
try:
|
||||||
|
guanshi.fenghong_erci = Decimal(str(perm_amount))
|
||||||
|
except:
|
||||||
|
return Response({'code': 400, 'msg': '永久分红金额格式错误'})
|
||||||
|
guanshi.save()
|
||||||
|
|
||||||
|
# 4.2 首次定制分红(cishu=1)
|
||||||
|
if custom_first is not None and isinstance(custom_first, dict):
|
||||||
|
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||||
|
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||||
|
# 获取当前所有首次定制记录
|
||||||
|
existing_first = DuociFenhong.query.filter(
|
||||||
|
club_id=guanshi_club, yonghuid=yonghuid, cishu=1,
|
||||||
|
)
|
||||||
|
# 前端传来的定制字典 {会员ID: 金额}
|
||||||
|
for huiyuan_id, amount in custom_first.items():
|
||||||
|
try:
|
||||||
|
amount = Decimal(str(amount))
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
if amount <= 0:
|
||||||
|
DuociFenhong.query.filter(
|
||||||
|
club_id=guanshi_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
|
||||||
|
).delete()
|
||||||
|
else:
|
||||||
|
DuociFenhong.query.update_or_create(
|
||||||
|
club_id=guanshi_club,
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
huiyuan=huiyuan_id,
|
||||||
|
cishu=1,
|
||||||
|
defaults={
|
||||||
|
'guanshi_fenhong': amount,
|
||||||
|
'zuzhang_fenhong': Decimal('0'),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
|
||||||
|
for record in existing_first:
|
||||||
|
if record.huiyuan not in custom_first:
|
||||||
|
record.delete()
|
||||||
|
|
||||||
|
# 4.3 额外次数分红(cishu >= 2)
|
||||||
|
if extra_map is not None and isinstance(extra_map, dict):
|
||||||
|
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||||
|
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||||
|
# 收集需要保留的记录
|
||||||
|
keep_set = set()
|
||||||
|
for times_str, members in extra_map.items():
|
||||||
|
try:
|
||||||
|
cishu = int(times_str)
|
||||||
|
if cishu < 2:
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
if not isinstance(members, dict):
|
||||||
|
continue
|
||||||
|
for huiyuan_id, data in members.items():
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
continue
|
||||||
|
amount = data.get('guanshi_fenhong')
|
||||||
|
if amount is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
amount = Decimal(str(amount))
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
keep_set.add((cishu, huiyuan_id))
|
||||||
|
# 更新或创建
|
||||||
|
DuociFenhong.query.update_or_create(
|
||||||
|
club_id=guanshi_club,
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
huiyuan=huiyuan_id,
|
||||||
|
cishu=cishu,
|
||||||
|
defaults={
|
||||||
|
'guanshi_fenhong': amount,
|
||||||
|
'zuzhang_fenhong': Decimal('0')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# 删除不在 keep_set 中的额外次数记录
|
||||||
|
existing_extra = DuociFenhong.query.filter(
|
||||||
|
club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2,
|
||||||
|
)
|
||||||
|
for record in existing_extra:
|
||||||
|
if (record.cishu, record.huiyuan) not in keep_set:
|
||||||
|
record.delete()
|
||||||
|
|
||||||
|
dividend_parts = []
|
||||||
|
if perm_enabled is not None:
|
||||||
|
dividend_parts.append(f'永久分红开关→{"开" if perm_enabled else "关"}')
|
||||||
|
if perm_amount is not None:
|
||||||
|
dividend_parts.append(f'永久分红金额→{perm_amount}')
|
||||||
|
if custom_first is not None:
|
||||||
|
dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)')
|
||||||
|
if extra_map is not None:
|
||||||
|
dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)')
|
||||||
|
write_xiugai_log(
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
xiugaiid=operator,
|
||||||
|
leixing=XIUGAI_LEIXING_GUANSHI,
|
||||||
|
qitashuoming=(
|
||||||
|
f'【后台】修改管事分红策略:{";".join(dividend_parts) or "配置已更新"},'
|
||||||
|
f'管事ID={yonghuid},操作人={operator}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response({'code': 0, 'msg': '修改成功'})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
1150
backend/views/kefu.py
Normal file
1150
backend/views/kefu.py
Normal file
File diff suppressed because it is too large
Load Diff
351
backend/views/members.py
Normal file
351
backend/views/members.py
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
import hmac
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import xmltodict
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from calendar import monthrange
|
||||||
|
from decimal import Decimal
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# 工具类导入
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action,
|
||||||
|
update_shangjia_daily
|
||||||
|
)
|
||||||
|
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||||
|
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||||
|
from jituan.services.club_penalty import (
|
||||||
|
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||||
|
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||||
|
)
|
||||||
|
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
from ..utils import (
|
||||||
|
verify_kefu_permission,
|
||||||
|
PERMISSION_TO_SHENFEN,
|
||||||
|
SHENFEN_PROFILE_MAP,
|
||||||
|
has_fadan_view_permission,
|
||||||
|
has_merchant_order_permission,
|
||||||
|
check_fadan_permission,
|
||||||
|
pick_order_id,
|
||||||
|
pick_penalty_create_params,
|
||||||
|
write_xiugai_log,
|
||||||
|
XIUGAI_LEIXING_DASHOU,
|
||||||
|
XIUGAI_LEIXING_GUANSHI,
|
||||||
|
XIUGAI_LEIXING_SHANGJIA,
|
||||||
|
XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
XIUGAI_LEIXING_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# models 集中导入
|
||||||
|
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||||
|
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||||
|
## backend
|
||||||
|
from backend.models import WithdrawalDailyStats
|
||||||
|
|
||||||
|
## users
|
||||||
|
from users.models import (
|
||||||
|
UserGuanshi, UserBoss, UserZuzhang,
|
||||||
|
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
## orders
|
||||||
|
from orders.models import (
|
||||||
|
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||||
|
OrderPlayerHistory, Order, RefundRecord,
|
||||||
|
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||||
|
)
|
||||||
|
|
||||||
|
## shop
|
||||||
|
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||||
|
|
||||||
|
## products
|
||||||
|
from products.models import (
|
||||||
|
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||||
|
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||||
|
)
|
||||||
|
|
||||||
|
## config
|
||||||
|
from config.models import (
|
||||||
|
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||||
|
TixianQuotaDefault,
|
||||||
|
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||||
|
)
|
||||||
|
|
||||||
|
## rank
|
||||||
|
from rank.models import (
|
||||||
|
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||||
|
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||||
|
)
|
||||||
|
|
||||||
|
# 序列化器
|
||||||
|
from ..serializers import PopupPageSerializer
|
||||||
|
|
||||||
|
# 全局常量、日志对象
|
||||||
|
logger = logging.getLogger('houtai')
|
||||||
|
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||||
|
|
||||||
|
class GetMemberListView(APIView):
|
||||||
|
"""
|
||||||
|
获取会员列表
|
||||||
|
权限:3300a
|
||||||
|
POST /houtai/hthqhylb
|
||||||
|
参数:username
|
||||||
|
返回:code:0, data: { list: [...] }
|
||||||
|
"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '3300a' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限查看会员列表'})
|
||||||
|
|
||||||
|
from jituan.services.club_member_admin import build_member_list_payload
|
||||||
|
|
||||||
|
payload = build_member_list_payload(request)
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {'list': payload['list']},
|
||||||
|
'club_id': payload.get('club_id'),
|
||||||
|
'scope': payload.get('scope'),
|
||||||
|
'price_note': payload.get('price_note'),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateMemberView(APIView):
|
||||||
|
"""
|
||||||
|
修改会员信息
|
||||||
|
权限:3300a
|
||||||
|
POST /houtai/htxghyxx
|
||||||
|
参数:
|
||||||
|
username, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
|
||||||
|
返回:code:0 成功
|
||||||
|
"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
huiyuan_id = request.data.get('huiyuan_id', '').strip()
|
||||||
|
jieshao = request.data.get('jieshao', '').strip()
|
||||||
|
jiage = request.data.get('jiage')
|
||||||
|
guanshifc = request.data.get('guanshifc')
|
||||||
|
zuzhangfc = request.data.get('zuzhangfc')
|
||||||
|
jtjieshao = request.data.get('jtjieshao', '').strip()
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
if not huiyuan_id:
|
||||||
|
return Response({'code': 400, 'msg': '缺少会员ID'})
|
||||||
|
if not jieshao:
|
||||||
|
return Response({'code': 400, 'msg': '会员名称不能为空'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
jiage = Decimal(str(jiage))
|
||||||
|
guanshifc = Decimal(str(guanshifc))
|
||||||
|
zuzhangfc = Decimal(str(zuzhangfc))
|
||||||
|
if jiage < 0 or guanshifc < 0 or zuzhangfc < 0:
|
||||||
|
raise ValueError
|
||||||
|
except:
|
||||||
|
return Response({'code': 400, 'msg': '金额格式错误'})
|
||||||
|
|
||||||
|
if guanshifc + zuzhangfc > jiage:
|
||||||
|
return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
|
||||||
|
|
||||||
|
# 权限校验
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '3300a' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改会员'})
|
||||||
|
|
||||||
|
from jituan.services.club_member_admin import update_member_for_club
|
||||||
|
|
||||||
|
extra = {
|
||||||
|
'formal_days': request.data.get('formal_days'),
|
||||||
|
'trial_enabled': request.data.get('trial_enabled'),
|
||||||
|
'trial_price': request.data.get('trial_price'),
|
||||||
|
'trial_days': request.data.get('trial_days'),
|
||||||
|
'trial_guanshifc': request.data.get('trial_guanshifc'),
|
||||||
|
'trial_zuzhangfc': request.data.get('trial_zuzhangfc'),
|
||||||
|
}
|
||||||
|
data, err = update_member_for_club(
|
||||||
|
request, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, extra=extra,
|
||||||
|
)
|
||||||
|
if err:
|
||||||
|
return Response({'code': 400, 'msg': err})
|
||||||
|
return Response({'code': 0, 'msg': '修改成功'})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AddMemberView(APIView):
|
||||||
|
"""
|
||||||
|
添加会员
|
||||||
|
权限:3300a
|
||||||
|
POST /houtai/httjhy
|
||||||
|
参数:
|
||||||
|
username, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
|
||||||
|
返回:code:0, data: { huiyuan_id }
|
||||||
|
"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
jieshao = request.data.get('jieshao', '').strip()
|
||||||
|
jiage = request.data.get('jiage')
|
||||||
|
guanshifc = request.data.get('guanshifc')
|
||||||
|
zuzhangfc = request.data.get('zuzhangfc')
|
||||||
|
jtjieshao = request.data.get('jtjieshao', '').strip()
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
if not jieshao:
|
||||||
|
return Response({'code': 400, 'msg': '会员名称不能为空'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
jiage = Decimal(str(jiage))
|
||||||
|
guanshifc = Decimal(str(guanshifc))
|
||||||
|
zuzhangfc = Decimal(str(zuzhangfc))
|
||||||
|
if jiage <= 0 or guanshifc < 0 or zuzhangfc < 0:
|
||||||
|
raise ValueError
|
||||||
|
except:
|
||||||
|
return Response({'code': 400, 'msg': '金额格式错误'})
|
||||||
|
|
||||||
|
if guanshifc + zuzhangfc > jiage:
|
||||||
|
return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
|
||||||
|
|
||||||
|
# 权限校验
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '3300a' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限添加会员'})
|
||||||
|
|
||||||
|
from jituan.services.club_member_admin import add_global_member
|
||||||
|
from jituan.services.club_context import resolve_club_scope
|
||||||
|
from jituan.constants import DATA_SCOPE_ALL
|
||||||
|
|
||||||
|
if resolve_club_scope(request) != DATA_SCOPE_ALL and '000001' not in permissions:
|
||||||
|
return Response({
|
||||||
|
'code': 403,
|
||||||
|
'msg': '仅集团汇总视图可新建全局会员类型;俱乐部请从会员 catalog 上架',
|
||||||
|
})
|
||||||
|
|
||||||
|
data, err = add_global_member(jieshao, jiage, guanshifc, zuzhangfc, jtjieshao)
|
||||||
|
if err:
|
||||||
|
return Response({'code': 400, 'msg': err})
|
||||||
|
return Response({'code': 0, 'msg': '添加成功', 'data': data})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ClubMemberCatalogView(APIView):
|
||||||
|
"""俱乐部可上架的集团会员 catalog。POST /houtai/hthycatalog"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '3300a' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限'})
|
||||||
|
from jituan.services.club_member_admin import build_member_catalog_payload
|
||||||
|
return Response({'code': 0, 'data': build_member_catalog_payload(request)})
|
||||||
|
|
||||||
|
|
||||||
|
class ClubMemberEnableView(APIView):
|
||||||
|
"""俱乐部上架会员。POST /houtai/hthysj"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
huiyuan_id = request.data.get('huiyuan_id', '').strip()
|
||||||
|
if not username or not huiyuan_id:
|
||||||
|
return Response({'code': 400, 'msg': '参数不完整'})
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '3300a' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限'})
|
||||||
|
try:
|
||||||
|
jiage = Decimal(str(request.data.get('jiage')))
|
||||||
|
guanshifc = Decimal(str(request.data.get('guanshifc')))
|
||||||
|
zuzhangfc = Decimal(str(request.data.get('zuzhangfc')))
|
||||||
|
except Exception:
|
||||||
|
return Response({'code': 400, 'msg': '金额格式错误'})
|
||||||
|
from jituan.services.club_member_admin import enable_member_for_club
|
||||||
|
extra = request.data
|
||||||
|
data, err = enable_member_for_club(
|
||||||
|
request, huiyuan_id, jiage, guanshifc, zuzhangfc, extra=extra,
|
||||||
|
)
|
||||||
|
if err:
|
||||||
|
return Response({'code': 400, 'msg': err})
|
||||||
|
return Response({'code': 0, 'msg': '上架成功', 'data': data})
|
||||||
|
|
||||||
|
|
||||||
|
class ClubMemberDisableView(APIView):
|
||||||
|
"""俱乐部下架会员。POST /houtai/hthyxj"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
huiyuan_id = request.data.get('huiyuan_id', '').strip()
|
||||||
|
if not username or not huiyuan_id:
|
||||||
|
return Response({'code': 400, 'msg': '参数不完整'})
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '3300a' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限'})
|
||||||
|
from jituan.services.club_member_admin import disable_member_for_club
|
||||||
|
data, err = disable_member_for_club(request, huiyuan_id)
|
||||||
|
if err:
|
||||||
|
return Response({'code': 400, 'msg': err})
|
||||||
|
return Response({'code': 0, 'msg': '已下架', 'data': data})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
733
backend/views/penalties.py
Normal file
733
backend/views/penalties.py
Normal file
@@ -0,0 +1,733 @@
|
|||||||
|
import hmac
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import xmltodict
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from calendar import monthrange
|
||||||
|
from decimal import Decimal
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# 工具类导入
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action,
|
||||||
|
update_shangjia_daily
|
||||||
|
)
|
||||||
|
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||||
|
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||||
|
from jituan.services.club_penalty import (
|
||||||
|
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||||
|
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||||
|
)
|
||||||
|
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
from ..utils import (
|
||||||
|
verify_kefu_permission,
|
||||||
|
PERMISSION_TO_SHENFEN,
|
||||||
|
SHENFEN_PROFILE_MAP,
|
||||||
|
has_fadan_view_permission,
|
||||||
|
has_merchant_order_permission,
|
||||||
|
check_fadan_permission,
|
||||||
|
pick_order_id,
|
||||||
|
pick_penalty_create_params,
|
||||||
|
write_xiugai_log,
|
||||||
|
XIUGAI_LEIXING_DASHOU,
|
||||||
|
XIUGAI_LEIXING_GUANSHI,
|
||||||
|
XIUGAI_LEIXING_SHANGJIA,
|
||||||
|
XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
XIUGAI_LEIXING_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# models 集中导入
|
||||||
|
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||||
|
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||||
|
## backend
|
||||||
|
from backend.models import WithdrawalDailyStats
|
||||||
|
|
||||||
|
## users
|
||||||
|
from users.models import (
|
||||||
|
UserGuanshi, UserBoss, UserZuzhang,
|
||||||
|
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
## orders
|
||||||
|
from orders.models import (
|
||||||
|
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||||
|
OrderPlayerHistory, Order, RefundRecord,
|
||||||
|
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||||
|
)
|
||||||
|
|
||||||
|
## shop
|
||||||
|
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||||
|
|
||||||
|
## products
|
||||||
|
from products.models import (
|
||||||
|
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||||
|
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||||
|
)
|
||||||
|
|
||||||
|
## config
|
||||||
|
from config.models import (
|
||||||
|
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||||
|
TixianQuotaDefault,
|
||||||
|
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||||
|
)
|
||||||
|
|
||||||
|
## rank
|
||||||
|
from rank.models import (
|
||||||
|
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||||
|
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||||
|
)
|
||||||
|
|
||||||
|
# 序列化器
|
||||||
|
from ..serializers import PopupPageSerializer
|
||||||
|
|
||||||
|
# 全局常量、日志对象
|
||||||
|
logger = logging.getLogger('houtai')
|
||||||
|
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||||
|
|
||||||
|
class FaKuanTongJiView(APIView):
|
||||||
|
"""
|
||||||
|
罚款统计接口
|
||||||
|
POST /houtai/htglyhqcfsltj
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 400, 'msg': '参数不完整'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
if not has_fadan_view_permission(permissions, username):
|
||||||
|
return Response({'code': 403, 'msg': '无权限查看罚款统计'})
|
||||||
|
|
||||||
|
from utils.penalty_status import penalty_platform_audit_pending_q
|
||||||
|
pending_q = penalty_platform_audit_pending_q()
|
||||||
|
stats = filter_penalty_qs(Penalty.query.all(), request).aggregate(
|
||||||
|
total=Count('id'),
|
||||||
|
daijiaona=Count('id', filter=Q(Status=1)),
|
||||||
|
shensuzhong=Count('id', filter=Q(Status=3)),
|
||||||
|
yijiaona=Count('id', filter=Q(Status=2)),
|
||||||
|
yibohui=Count('id', filter=Q(Status=4)),
|
||||||
|
pingtai_shenhe=Count('id', filter=pending_q),
|
||||||
|
)
|
||||||
|
data = {k: v or 0 for k, v in stats.items()}
|
||||||
|
from jituan.services.club_user_access import list_response_meta
|
||||||
|
return Response({'code': 0, 'msg': '成功', 'data': {**data, **list_response_meta(request)}})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"罚款统计异常: {traceback.format_exc()}")
|
||||||
|
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||||
|
|
||||||
|
|
||||||
|
class FaKuanLieBiaoView(APIView):
|
||||||
|
"""
|
||||||
|
罚款列表接口
|
||||||
|
POST /houtai/hthqfklb
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 400, 'msg': '参数不完整'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if not has_fadan_view_permission(permissions, username):
|
||||||
|
return Response({'code': 403, 'msg': '无权限查看罚款列表'})
|
||||||
|
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('page_size', 20))
|
||||||
|
if page_size > 100:
|
||||||
|
page_size = 100
|
||||||
|
|
||||||
|
qs = filter_penalty_qs(Penalty.query.all(), request)
|
||||||
|
|
||||||
|
# 状态筛选(5=平台审核中,仅 zhuangtai=5)
|
||||||
|
from utils.penalty_status import penalty_platform_audit_pending_q
|
||||||
|
zhuangtai_raw = request.data.get('zhuangtai')
|
||||||
|
if zhuangtai_raw is not None and str(zhuangtai_raw).strip() != '':
|
||||||
|
try:
|
||||||
|
zt = int(zhuangtai_raw)
|
||||||
|
if zt == 5:
|
||||||
|
qs = qs.filter(penalty_platform_audit_pending_q())
|
||||||
|
elif zt in (1, 2, 3, 4):
|
||||||
|
qs = qs.filter(Status=zt)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 被处罚人ID
|
||||||
|
PenalizedUserID = request.data.get('PenalizedUserID', '').strip()
|
||||||
|
if PenalizedUserID:
|
||||||
|
qs = qs.filter(PenalizedUserID=PenalizedUserID)
|
||||||
|
|
||||||
|
# 被处罚人身份
|
||||||
|
beichufa_shenfen = request.data.get('beichufa_shenfen')
|
||||||
|
if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]:
|
||||||
|
qs = qs.filter(Identity=int(beichufa_shenfen))
|
||||||
|
|
||||||
|
# 模糊搜索(订单ID、被罚人ID、申请人ID)
|
||||||
|
sousuo = request.data.get('sousuo', '').strip()
|
||||||
|
if sousuo:
|
||||||
|
qs = qs.filter(
|
||||||
|
Q(RelatedOrderID__icontains=sousuo) |
|
||||||
|
Q(PenalizedUserID__icontains=sousuo) |
|
||||||
|
Q(ApplicantID__icontains=sousuo)
|
||||||
|
)
|
||||||
|
|
||||||
|
qs = qs.order_by('-CreateTime')
|
||||||
|
paginator = Paginator(qs, page_size)
|
||||||
|
try:
|
||||||
|
page_obj = paginator.page(page)
|
||||||
|
except Exception:
|
||||||
|
return Response({
|
||||||
|
'code': 0, 'msg': '成功',
|
||||||
|
'data': {'list': [], 'total': 0, 'page': page, 'page_size': page_size}
|
||||||
|
})
|
||||||
|
|
||||||
|
records = list(page_obj)
|
||||||
|
penalty_ids = [r.id for r in records]
|
||||||
|
|
||||||
|
# 批量获取图片并分组
|
||||||
|
zhengju_map = {}
|
||||||
|
shensu_map = {}
|
||||||
|
if penalty_ids:
|
||||||
|
tupians = PenaltyAppealImage.query.filter(
|
||||||
|
Penalty_id__in=penalty_ids
|
||||||
|
).values('Penalty_id', 'ImageURL', 'Purpose')
|
||||||
|
for t in tupians:
|
||||||
|
fid = t['Penalty_id']
|
||||||
|
url = t['ImageURL']
|
||||||
|
if t['Purpose'] == 1:
|
||||||
|
zhengju_map.setdefault(fid, []).append(url)
|
||||||
|
else:
|
||||||
|
shensu_map.setdefault(fid, []).append(url)
|
||||||
|
|
||||||
|
from utils.penalty_status import penalty_display_status, is_penalty_platform_audit_pending
|
||||||
|
|
||||||
|
# 组装列表数据
|
||||||
|
results = []
|
||||||
|
for r in records:
|
||||||
|
results.append({
|
||||||
|
'id': r.id,
|
||||||
|
'beichufa_id': r.PenalizedUserID,
|
||||||
|
'shenfen': r.Identity,
|
||||||
|
'shenfen_text': dict(Penalty._meta.get_field('Identity').choices).get(r.Identity, ''),
|
||||||
|
'shenqing_chufa': r.ApplicantID or '',
|
||||||
|
'shenqingren_shenfen': r.ApplicantIdentity,
|
||||||
|
'chufaliyou': r.Reason or '',
|
||||||
|
'fakuanjine': str(r.FineAmount),
|
||||||
|
'guanliandingdan_id': r.RelatedOrderID or '',
|
||||||
|
'zhuangtai': r.Status,
|
||||||
|
'zhuangtai_text': penalty_display_status(r),
|
||||||
|
'platform_audit_pending': is_penalty_platform_audit_pending(r),
|
||||||
|
'yingxiang_qiangdan': r.AffectsGrabbing,
|
||||||
|
'shensuliyou': r.AppealReason or '',
|
||||||
|
'bohuiliyou': r.RejectReason or '',
|
||||||
|
'chulizhe': r.ProcessorID or '',
|
||||||
|
'chulizhe_shenfen': r.ProcessorIdentity,
|
||||||
|
'zhengju_tupian': zhengju_map.get(r.id, []),
|
||||||
|
'shensu_tupian': shensu_map.get(r.id, []),
|
||||||
|
'creat_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||||||
|
'create_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
|
||||||
|
'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '',
|
||||||
|
})
|
||||||
|
|
||||||
|
from jituan.services.club_user_access import list_response_meta
|
||||||
|
return Response({
|
||||||
|
'code': 0, 'msg': '成功',
|
||||||
|
'data': {
|
||||||
|
'list': results,
|
||||||
|
'total': paginator.count,
|
||||||
|
'page': page,
|
||||||
|
'page_size': page_size,
|
||||||
|
**list_response_meta(request),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"罚款列表异常: {traceback.format_exc()}")
|
||||||
|
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||||
|
|
||||||
|
|
||||||
|
class FaKuanChuLiView(APIView):
|
||||||
|
"""
|
||||||
|
处理罚款申诉(同意/拒绝)
|
||||||
|
POST /houtai/glyclfk
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id')
|
||||||
|
action = request.data.get('action', '').strip()
|
||||||
|
chuli_liyou = request.data.get('chuli_liyou', '').strip()
|
||||||
|
|
||||||
|
if not all([username, penalty_id, action]):
|
||||||
|
return Response({'code': 400, 'msg': '参数不完整'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
try:
|
||||||
|
penalty = Penalty.objects.select_for_update().get(id=penalty_id)
|
||||||
|
except Penalty.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '罚单不存在'})
|
||||||
|
|
||||||
|
deny = forbid_penalty_out_of_scope(request, penalty)
|
||||||
|
if deny:
|
||||||
|
return deny
|
||||||
|
|
||||||
|
if penalty.Status != 3:
|
||||||
|
return Response({'code': 400, 'msg': '当前状态不可处理'})
|
||||||
|
|
||||||
|
if not check_fadan_permission(permissions, penalty.Identity):
|
||||||
|
return Response({'code': 403, 'msg': '无权限处理该类罚单'})
|
||||||
|
|
||||||
|
# 处理上传的图片
|
||||||
|
uploaded_urls = []
|
||||||
|
files = request.FILES.getlist('tupian') or []
|
||||||
|
if not files:
|
||||||
|
single = request.FILES.get('tupian')
|
||||||
|
if single:
|
||||||
|
files = [single]
|
||||||
|
for file in files:
|
||||||
|
valid, msg = validate_image(file)
|
||||||
|
if not valid:
|
||||||
|
return Response({'code': 400, 'msg': msg})
|
||||||
|
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
|
||||||
|
file_name = f"chufa_{penalty_id}_{int(time.time() * 1000)}.{ext}"
|
||||||
|
file_path = f"chufatupian/kechufatupian/{file_name}"
|
||||||
|
url = upload_to_oss(file, file_path)
|
||||||
|
if not url:
|
||||||
|
return Response({'code': 500, 'msg': '图片上传失败'})
|
||||||
|
uploaded_urls.append(file_path)
|
||||||
|
|
||||||
|
# 更新状态
|
||||||
|
if action == 'agree': # 同意申诉 → 驳回处罚
|
||||||
|
penalty.Status = 4
|
||||||
|
penalty.RejectReason = chuli_liyou
|
||||||
|
elif action == 'reject': # 拒绝申诉 → 回到待缴纳
|
||||||
|
penalty.Status = 1
|
||||||
|
penalty.RejectReason = chuli_liyou
|
||||||
|
else:
|
||||||
|
return Response({'code': 400, 'msg': '无效的操作类型'})
|
||||||
|
|
||||||
|
# 记录处理者(使用 request.user 的 phone)
|
||||||
|
penalty.ProcessorID = request.user.Phone
|
||||||
|
penalty.ProcessorIdentity = 2 # 这里假设处理者是售后身份,可根据实际业务获取
|
||||||
|
penalty.save()
|
||||||
|
|
||||||
|
# 保存图片(证据图片,用途1)
|
||||||
|
for relative_url in uploaded_urls:
|
||||||
|
PenaltyAppealImage.query.create(
|
||||||
|
Penalty_id=penalty_id,
|
||||||
|
PenalizedUserID=penalty.PenalizedUserID,
|
||||||
|
ImageURL=relative_url,
|
||||||
|
Purpose=1
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"罚款处理成功: {penalty_id}, 操作: {action}")
|
||||||
|
return Response({'code': 0, 'msg': '处理成功'})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"罚款处理异常: {traceback.format_exc()}")
|
||||||
|
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||||
|
|
||||||
|
|
||||||
|
class FaKuanPingTaiShenHeView(APIView):
|
||||||
|
"""
|
||||||
|
平台审核商家提交的罚款(同意/驳回)
|
||||||
|
POST /houtai/glyptshfk
|
||||||
|
action: platform_approve | platform_reject
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
from utils.penalty_status import (
|
||||||
|
PENALTY_PLATFORM_AUDIT, PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_REJECTED,
|
||||||
|
)
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id')
|
||||||
|
action = request.data.get('action', '').strip()
|
||||||
|
chuli_liyou = request.data.get('chuli_liyou', '').strip()
|
||||||
|
|
||||||
|
if not all([username, penalty_id, action]):
|
||||||
|
return Response({'code': 400, 'msg': '参数不完整'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
try:
|
||||||
|
penalty = Penalty.objects.select_for_update().get(id=penalty_id)
|
||||||
|
except Penalty.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '罚单不存在'})
|
||||||
|
|
||||||
|
deny = forbid_penalty_out_of_scope(request, penalty)
|
||||||
|
if deny:
|
||||||
|
return deny
|
||||||
|
|
||||||
|
if penalty.Status != PENALTY_PLATFORM_AUDIT:
|
||||||
|
return Response({'code': 400, 'msg': '当前状态不可平台审核'})
|
||||||
|
|
||||||
|
if not check_fadan_permission(permissions, penalty.Identity):
|
||||||
|
return Response({'code': 403, 'msg': '无权限处理该类罚单'})
|
||||||
|
|
||||||
|
if action == 'platform_approve':
|
||||||
|
if penalty.AppealReason:
|
||||||
|
penalty.Status = PENALTY_APPEALING
|
||||||
|
else:
|
||||||
|
penalty.Status = PENALTY_PENDING_PAY
|
||||||
|
penalty.ApproveReason = chuli_liyou or penalty.ApproveReason or ''
|
||||||
|
elif action == 'platform_reject':
|
||||||
|
penalty.Status = PENALTY_REJECTED
|
||||||
|
penalty.RejectReason = chuli_liyou or '平台审核驳回'
|
||||||
|
else:
|
||||||
|
return Response({'code': 400, 'msg': '无效的操作类型'})
|
||||||
|
|
||||||
|
penalty.ProcessorID = request.user.Phone
|
||||||
|
penalty.ProcessorIdentity = 3
|
||||||
|
penalty.save()
|
||||||
|
|
||||||
|
logger.info(f"罚款平台审核成功: {penalty_id}, 操作: {action}")
|
||||||
|
return Response({'code': 0, 'msg': '平台审核完成'})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"罚款平台审核异常: {traceback.format_exc()}")
|
||||||
|
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||||
|
|
||||||
|
|
||||||
|
class FaKuanChuangJianView(APIView):
|
||||||
|
"""
|
||||||
|
管理员创建罚款
|
||||||
|
POST /houtai/htfksc
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
PenalizedUserID, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data)
|
||||||
|
shenfen = request.data.get('shenfen')
|
||||||
|
|
||||||
|
if not all([username, PenalizedUserID, shenfen, Reason, FineAmount is not None]):
|
||||||
|
return Response({'code': 400, 'msg': '参数不完整'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
shenfen = int(shenfen)
|
||||||
|
FineAmount = float(FineAmount)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return Response({'code': 400, 'msg': '参数格式错误'})
|
||||||
|
|
||||||
|
if shenfen not in [1, 2, 3, 4]:
|
||||||
|
return Response({'code': 400, 'msg': '无效的被罚人身份'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
if not check_fadan_permission(permissions, shenfen):
|
||||||
|
return Response({'code': 403, 'msg': '无权限创建该类罚单'})
|
||||||
|
|
||||||
|
# 验证用户是否存在对应的扩展表
|
||||||
|
try:
|
||||||
|
user = User.query.get(UserUID=PenalizedUserID)
|
||||||
|
profile_attr = SHENFEN_PROFILE_MAP.get(shenfen)
|
||||||
|
if not profile_attr or not hasattr(user, profile_attr):
|
||||||
|
return Response({'code': 400, 'msg': '该用户不是所选身份'})
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '用户不存在'})
|
||||||
|
|
||||||
|
# 处理图片上传
|
||||||
|
uploaded_urls = []
|
||||||
|
files = request.FILES.getlist('tupian') or []
|
||||||
|
if not files:
|
||||||
|
single = request.FILES.get('tupian')
|
||||||
|
if single:
|
||||||
|
files = [single]
|
||||||
|
for file in files:
|
||||||
|
valid, msg = validate_image(file)
|
||||||
|
if not valid:
|
||||||
|
return Response({'code': 400, 'msg': msg})
|
||||||
|
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
|
||||||
|
file_name = f"chufa_{int(time.time() * 1000)}.{ext}"
|
||||||
|
file_path = f"chufatupian/kechufatupian/{file_name}"
|
||||||
|
url = upload_to_oss(file, file_path)
|
||||||
|
if not url:
|
||||||
|
return Response({'code': 500, 'msg': '图片上传失败'})
|
||||||
|
uploaded_urls.append(file_path)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
penalty = Penalty.query.create(
|
||||||
|
PenalizedUserID=PenalizedUserID,
|
||||||
|
Identity=shenfen,
|
||||||
|
ApplicantID=request.user.Phone, # 申请人(客服)手机号
|
||||||
|
ApplicantIdentity=2, # 售后身份
|
||||||
|
Reason=Reason,
|
||||||
|
FineAmount=FineAmount,
|
||||||
|
AffectsGrabbing=AffectsGrabbing,
|
||||||
|
Status=1, # 待缴纳
|
||||||
|
ClubID=resolve_penalty_club_id(
|
||||||
|
penalized_user_id=PenalizedUserID,
|
||||||
|
request=request,
|
||||||
|
user=request.user,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for relative_url in uploaded_urls:
|
||||||
|
PenaltyAppealImage.query.create(
|
||||||
|
Penalty=penalty,
|
||||||
|
PenalizedUserID=PenalizedUserID,
|
||||||
|
ImageURL=relative_url,
|
||||||
|
Purpose=1
|
||||||
|
)
|
||||||
|
from users.fadan_fenhong_utils import lock_penalty_bonus
|
||||||
|
lock_penalty_bonus(penalty)
|
||||||
|
|
||||||
|
logger.info(f"罚款创建成功: {penalty.id}")
|
||||||
|
return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': penalty.id}})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"创建罚款异常: {traceback.format_exc()}")
|
||||||
|
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||||
|
|
||||||
|
|
||||||
|
class FineApplyView(APIView):
|
||||||
|
"""
|
||||||
|
客服罚款申请接口(基于订单对打手发起罚款)
|
||||||
|
POST /houtai/kffkdssq
|
||||||
|
权限: JWT认证 + 客服权限验证
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
username (必填) : 客服用户名(手机号)
|
||||||
|
OrderID (必填) : 订单ID
|
||||||
|
Reason (必填) : 罚款原因
|
||||||
|
FineAmount (必填) : 罚款金额(元)
|
||||||
|
AffectsGrabbing (可选) : 是否影响抢单,1=是,0=否,默认1
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
OrderID = pick_order_id(request.data)
|
||||||
|
_, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data)
|
||||||
|
|
||||||
|
if not all([username, OrderID, Reason, FineAmount is not None]):
|
||||||
|
return Response({'code': 400, 'msg': '参数不完整'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
FineAmount = float(FineAmount)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return Response({'code': 400, 'msg': '罚款金额格式错误'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
# 验证订单存在
|
||||||
|
try:
|
||||||
|
order = Order.query.get(OrderID=OrderID)
|
||||||
|
except Order.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '订单不存在'})
|
||||||
|
|
||||||
|
# 获取打手ID
|
||||||
|
dashou_id = order.PlayerID
|
||||||
|
if not dashou_id:
|
||||||
|
return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'})
|
||||||
|
|
||||||
|
# 重复罚款检查
|
||||||
|
if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=OrderID).exists():
|
||||||
|
return Response({'code': 400, 'msg': '该打手已被罚款过'})
|
||||||
|
|
||||||
|
# 检查权限(打手身份 shenfen=1)
|
||||||
|
if not check_fadan_permission(permissions, 1):
|
||||||
|
return Response({'code': 403, 'msg': '无权限处罚打手'})
|
||||||
|
|
||||||
|
# 处理图片上传
|
||||||
|
uploaded_urls = []
|
||||||
|
files = request.FILES.getlist('tupian') or []
|
||||||
|
if not files:
|
||||||
|
single = request.FILES.get('tupian')
|
||||||
|
if single:
|
||||||
|
files = [single]
|
||||||
|
for file in files:
|
||||||
|
valid, msg = validate_image(file)
|
||||||
|
if not valid:
|
||||||
|
return Response({'code': 400, 'msg': msg})
|
||||||
|
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
|
||||||
|
file_name = f"chufa_{int(time.time() * 1000)}.{ext}"
|
||||||
|
file_path = f"chufatupian/kechufatupian/{file_name}"
|
||||||
|
url = upload_to_oss(file, file_path)
|
||||||
|
if not url:
|
||||||
|
return Response({'code': 500, 'msg': '图片上传失败'})
|
||||||
|
uploaded_urls.append(file_path)
|
||||||
|
|
||||||
|
# 创建罚单
|
||||||
|
with transaction.atomic():
|
||||||
|
penalty = Penalty.query.create(
|
||||||
|
PenalizedUserID=dashou_id,
|
||||||
|
ApplicantID=request.user.Phone,
|
||||||
|
Identity=1, # 被处罚人身份:1 打手
|
||||||
|
Reason=Reason,
|
||||||
|
FineAmount=FineAmount,
|
||||||
|
RelatedOrderID=OrderID,
|
||||||
|
Status=1, # 待缴纳
|
||||||
|
AffectsGrabbing=AffectsGrabbing,
|
||||||
|
ApplicantIdentity=1, # 申请人身份:1 客服
|
||||||
|
ClubID=resolve_penalty_club_id(
|
||||||
|
order=order,
|
||||||
|
request=request,
|
||||||
|
user=request.user,
|
||||||
|
applicant_id=getattr(request.user, 'UserUID', None),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for relative_url in uploaded_urls:
|
||||||
|
PenaltyAppealImage.query.create(
|
||||||
|
Penalty=penalty,
|
||||||
|
PenalizedUserID=dashou_id,
|
||||||
|
ImageURL=relative_url,
|
||||||
|
Purpose=1
|
||||||
|
)
|
||||||
|
from users.fadan_fenhong_utils import lock_penalty_bonus
|
||||||
|
lock_penalty_bonus(penalty)
|
||||||
|
|
||||||
|
logger.info(f"客服罚款申请成功: 订单 {OrderID}, 打手 {dashou_id}, 金额 {FineAmount}")
|
||||||
|
return Response({'code': 0, 'msg': '罚款已生成', 'data': {'id': penalty.id}})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"客服罚款申请异常: {traceback.format_exc()}")
|
||||||
|
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||||
|
|
||||||
|
|
||||||
|
class PunishDashouView(APIView):
|
||||||
|
"""
|
||||||
|
客服处罚打手接口(扣除积分)
|
||||||
|
POST /houtai/kpcf
|
||||||
|
权限: JWT认证 + 客服权限验证
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
username (必填) : 客服用户名(手机号)
|
||||||
|
OrderID (必填) : 订单ID
|
||||||
|
reason (可选) : 处罚原因
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
OrderID = request.data.get('OrderID', '').strip()
|
||||||
|
reason = request.data.get('reason', '').strip()
|
||||||
|
|
||||||
|
if not all([username, OrderID]):
|
||||||
|
return Response({'code': 400, 'msg': '参数不完整'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
# 检查权限(打手身份 shenfen=1)
|
||||||
|
if not check_fadan_permission(permissions, 1):
|
||||||
|
return Response({'code': 403, 'msg': '无权限处罚打手'})
|
||||||
|
|
||||||
|
# 查询订单
|
||||||
|
try:
|
||||||
|
order = Order.query.get(OrderID=OrderID)
|
||||||
|
except Order.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '订单不存在'})
|
||||||
|
|
||||||
|
# 获取打手ID
|
||||||
|
dashou_id = order.PlayerID
|
||||||
|
if not dashou_id:
|
||||||
|
return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'})
|
||||||
|
|
||||||
|
# 查询打手
|
||||||
|
try:
|
||||||
|
dashou_user = User.query.get(UserUID=dashou_id)
|
||||||
|
dashou = dashou_user.DashouProfile
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '打手用户不存在'})
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return Response({'code': 400, 'msg': '该用户不是打手'})
|
||||||
|
|
||||||
|
# 创建处罚记录(扣除积分5分)
|
||||||
|
jifen = 5
|
||||||
|
with transaction.atomic():
|
||||||
|
penalty_record = PenaltyRecord.query.create(
|
||||||
|
PlayerID=dashou_id,
|
||||||
|
ApplicantID=username,
|
||||||
|
PenaltyReason=reason or '',
|
||||||
|
ApplyStatus=0,
|
||||||
|
DeductedPoints=jifen,
|
||||||
|
OrderID=OrderID,
|
||||||
|
ClubID=resolve_penalty_record_club_id(
|
||||||
|
order_id=OrderID,
|
||||||
|
player_id=dashou_id,
|
||||||
|
request=request,
|
||||||
|
user=request.user,
|
||||||
|
applicant_id=getattr(request.user, 'UserUID', None) or username,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# 扣除打手积分
|
||||||
|
dashou.jifen = F('jifen') - jifen
|
||||||
|
dashou.save(update_fields=['jifen'])
|
||||||
|
|
||||||
|
logger.info(f"客服处罚打手成功: 打手 {dashou_id}, 订单 {OrderID}, 扣除积分 {jifen}")
|
||||||
|
return Response({'code': 0, 'msg': '处罚成功', 'data': {'penalty_record_id': penalty_record.id}})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"客服处罚打手异常: {traceback.format_exc()}")
|
||||||
|
return Response({'code': 500, 'msg': '系统繁忙'})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
1140
backend/views/products.py
Normal file
1140
backend/views/products.py
Normal file
File diff suppressed because it is too large
Load Diff
806
backend/views/roles.py
Normal file
806
backend/views/roles.py
Normal file
@@ -0,0 +1,806 @@
|
|||||||
|
import hmac
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import xmltodict
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from calendar import monthrange
|
||||||
|
from decimal import Decimal
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# 工具类导入
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action,
|
||||||
|
update_shangjia_daily
|
||||||
|
)
|
||||||
|
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||||
|
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||||
|
from jituan.services.club_penalty import (
|
||||||
|
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||||
|
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||||
|
)
|
||||||
|
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
from ..utils import (
|
||||||
|
verify_kefu_permission,
|
||||||
|
PERMISSION_TO_SHENFEN,
|
||||||
|
SHENFEN_PROFILE_MAP,
|
||||||
|
has_fadan_view_permission,
|
||||||
|
has_merchant_order_permission,
|
||||||
|
check_fadan_permission,
|
||||||
|
pick_order_id,
|
||||||
|
pick_penalty_create_params,
|
||||||
|
write_xiugai_log,
|
||||||
|
XIUGAI_LEIXING_DASHOU,
|
||||||
|
XIUGAI_LEIXING_GUANSHI,
|
||||||
|
XIUGAI_LEIXING_SHANGJIA,
|
||||||
|
XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
XIUGAI_LEIXING_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# models 集中导入
|
||||||
|
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||||
|
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||||
|
## backend
|
||||||
|
from backend.models import WithdrawalDailyStats
|
||||||
|
|
||||||
|
## users
|
||||||
|
from users.models import (
|
||||||
|
UserGuanshi, UserBoss, UserZuzhang,
|
||||||
|
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
## orders
|
||||||
|
from orders.models import (
|
||||||
|
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||||
|
OrderPlayerHistory, Order, RefundRecord,
|
||||||
|
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||||
|
)
|
||||||
|
|
||||||
|
## shop
|
||||||
|
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||||
|
|
||||||
|
## products
|
||||||
|
from products.models import (
|
||||||
|
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||||
|
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||||
|
)
|
||||||
|
|
||||||
|
## config
|
||||||
|
from config.models import (
|
||||||
|
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||||
|
TixianQuotaDefault,
|
||||||
|
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||||
|
)
|
||||||
|
|
||||||
|
## rank
|
||||||
|
from rank.models import (
|
||||||
|
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||||
|
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||||
|
)
|
||||||
|
|
||||||
|
# 序列化器
|
||||||
|
from ..serializers import PopupPageSerializer
|
||||||
|
|
||||||
|
# 全局常量、日志对象
|
||||||
|
logger = logging.getLogger('houtai')
|
||||||
|
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||||
|
|
||||||
|
def _serialize_permission_row(perm_row):
|
||||||
|
"""gvsdsdk Permission → 后台 Vue 约定的小写字段"""
|
||||||
|
if isinstance(perm_row, dict):
|
||||||
|
code = perm_row.get('PermCode') or perm_row.get('perm_code') or ''
|
||||||
|
name = perm_row.get('PermName') or perm_row.get('perm_name') or code
|
||||||
|
desc = perm_row.get('PermDesc') or perm_row.get('perm_desc') or ''
|
||||||
|
else:
|
||||||
|
code = perm_row.PermCode or ''
|
||||||
|
name = perm_row.PermName or code
|
||||||
|
desc = perm_row.PermDesc or ''
|
||||||
|
return {'perm_code': code, 'perm_name': name, 'perm_desc': desc}
|
||||||
|
|
||||||
|
|
||||||
|
class GetRolePermissionView(APIView):
|
||||||
|
"""
|
||||||
|
获取所有角色、权限以及角色关联的用户数
|
||||||
|
权限要求:用户必须拥有 000001 权限
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 400, 'msg': '缺少username'})
|
||||||
|
|
||||||
|
# 1. 权限校验(验证客服身份,并获取权限列表)
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions # 直接返回错误响应
|
||||||
|
|
||||||
|
# 2. 检查是否有 000001 权限
|
||||||
|
if '000001' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||||||
|
|
||||||
|
# 3. 获取所有权限
|
||||||
|
all_perms = Permission.objects.filter(PermStatus=1).values('PermCode', 'PermName', 'PermDesc')
|
||||||
|
permissions_list = [_serialize_permission_row(p) for p in all_perms]
|
||||||
|
|
||||||
|
from jituan.services.club_rbac import count_role_users_in_scope, filter_roles_for_request
|
||||||
|
|
||||||
|
kefu_user_uuids = list(
|
||||||
|
User.objects.filter(KefuProfile__isnull=False).values_list('UserUUID', flat=True)
|
||||||
|
)
|
||||||
|
roles = filter_roles_for_request(request)
|
||||||
|
roles_data = []
|
||||||
|
for role in roles:
|
||||||
|
user_count = count_role_users_in_scope(role, request, kefu_user_uuids)
|
||||||
|
role_perm_uuids = RolePermission.objects.filter(
|
||||||
|
RoleUUID=role.RoleUUID
|
||||||
|
).values_list('PermUUID', flat=True)
|
||||||
|
perms_of_role = [
|
||||||
|
_serialize_permission_row(p)
|
||||||
|
for p in Permission.objects.filter(
|
||||||
|
PermUUID__in=list(role_perm_uuids)
|
||||||
|
).values('PermCode', 'PermName', 'PermDesc')
|
||||||
|
]
|
||||||
|
roles_data.append({
|
||||||
|
'role_code': role.RoleName,
|
||||||
|
'role_name': role.RoleName,
|
||||||
|
'description': role.RoleDesc or '',
|
||||||
|
'club_id': getattr(role, 'ClubID', '') or '',
|
||||||
|
'user_count': user_count,
|
||||||
|
'permissions': perms_of_role
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'roles': roles_data,
|
||||||
|
'permissions': permissions_list
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class ModifyRolePermissionView(APIView):
|
||||||
|
"""
|
||||||
|
通用接口:支持删除角色、为角色添加权限、移除角色权限
|
||||||
|
操作类型通过 action 字段区分
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
action = request.data.get('action') # delete_role, add_permission, remove_permission
|
||||||
|
if not username or not action:
|
||||||
|
return Response({'code': 400, 'msg': '缺少必要参数'})
|
||||||
|
|
||||||
|
# 权限校验
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '000001' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||||||
|
|
||||||
|
# 获取角色(需要角色名称)
|
||||||
|
role_code = request.data.get('role_code')
|
||||||
|
if not role_code:
|
||||||
|
return Response({'code': 400, 'msg': '缺少角色编码'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
role = Role.objects.get(RoleName=role_code)
|
||||||
|
except Role.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '角色不存在'})
|
||||||
|
|
||||||
|
# 超级管理员角色的特殊限制
|
||||||
|
if role.RoleName == '管理员':
|
||||||
|
if action in ['delete_role']:
|
||||||
|
return Response({'code': 403, 'msg': '超级管理员角色不能删除'})
|
||||||
|
|
||||||
|
# 根据 action 处理
|
||||||
|
if action == 'delete_role':
|
||||||
|
# 删除角色:同时删除角色权限关联、用户角色关联
|
||||||
|
with transaction.atomic():
|
||||||
|
RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete()
|
||||||
|
UserRole.objects.filter(RoleUUID=role.RoleUUID).delete()
|
||||||
|
role.delete()
|
||||||
|
return Response({'code': 0, 'msg': '角色删除成功'})
|
||||||
|
|
||||||
|
elif action == 'add_permission':
|
||||||
|
perm_code = request.data.get('perm_code')
|
||||||
|
if not perm_code:
|
||||||
|
return Response({'code': 400, 'msg': '缺少权限编码'})
|
||||||
|
if role.RoleName == '管理员' and perm_code == '000001':
|
||||||
|
return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'})
|
||||||
|
try:
|
||||||
|
perm = Permission.objects.get(PermCode=perm_code)
|
||||||
|
except Permission.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '权限不存在'})
|
||||||
|
# 检查是否已存在
|
||||||
|
if RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).exists():
|
||||||
|
return Response({'code': 400, 'msg': '该角色已拥有此权限'})
|
||||||
|
RolePermission.objects.create(
|
||||||
|
RolePermUUID=uuid.uuid4().bytes,
|
||||||
|
RoleUUID=role.RoleUUID,
|
||||||
|
PermUUID=perm.PermUUID,
|
||||||
|
CreateTime=timezone.now(),
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '权限添加成功'})
|
||||||
|
|
||||||
|
elif action == 'remove_permission':
|
||||||
|
perm_code = request.data.get('perm_code')
|
||||||
|
if not perm_code:
|
||||||
|
return Response({'code': 400, 'msg': '缺少权限编码'})
|
||||||
|
if role.RoleName == '管理员' and perm_code == '000001':
|
||||||
|
return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'})
|
||||||
|
try:
|
||||||
|
perm = Permission.objects.get(PermCode=perm_code)
|
||||||
|
except Permission.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '权限不存在'})
|
||||||
|
deleted, _ = RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).delete()
|
||||||
|
if deleted == 0:
|
||||||
|
return Response({'code': 400, 'msg': '该角色未拥有此权限'})
|
||||||
|
return Response({'code': 0, 'msg': '权限移除成功'})
|
||||||
|
|
||||||
|
elif action == 'update_role':
|
||||||
|
role_name = (request.data.get('role_name') or '').strip()
|
||||||
|
description = (request.data.get('description') or '').strip()
|
||||||
|
perm_codes = request.data.get('perm_codes') or []
|
||||||
|
if role.RoleName == '管理员':
|
||||||
|
role_name = role.RoleName
|
||||||
|
elif role_name and role_name != role.RoleName:
|
||||||
|
if Role.objects.filter(RoleName=role_name).exclude(RoleUUID=role.RoleUUID).exists():
|
||||||
|
return Response({'code': 400, 'msg': '角色名称已存在'})
|
||||||
|
with transaction.atomic():
|
||||||
|
if role.RoleName != '管理员':
|
||||||
|
if role_name:
|
||||||
|
role.RoleName = role_name
|
||||||
|
role.RoleDesc = description
|
||||||
|
role.save(update_fields=['RoleName', 'RoleDesc'])
|
||||||
|
RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete()
|
||||||
|
for pc in perm_codes:
|
||||||
|
if pc == '000001' and role.RoleName != '管理员':
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
perm = Permission.objects.get(PermCode=pc, PermStatus=1)
|
||||||
|
except Permission.DoesNotExist:
|
||||||
|
continue
|
||||||
|
RolePermission.objects.create(
|
||||||
|
RolePermUUID=uuid.uuid4().bytes,
|
||||||
|
RoleUUID=role.RoleUUID,
|
||||||
|
PermUUID=perm.PermUUID,
|
||||||
|
CreateTime=timezone.now(),
|
||||||
|
)
|
||||||
|
if role.RoleName == '管理员':
|
||||||
|
try:
|
||||||
|
super_perm = Permission.objects.get(PermCode='000001', PermStatus=1)
|
||||||
|
if not RolePermission.objects.filter(
|
||||||
|
RoleUUID=role.RoleUUID, PermUUID=super_perm.PermUUID,
|
||||||
|
).exists():
|
||||||
|
RolePermission.objects.create(
|
||||||
|
RolePermUUID=uuid.uuid4().bytes,
|
||||||
|
RoleUUID=role.RoleUUID,
|
||||||
|
PermUUID=super_perm.PermUUID,
|
||||||
|
CreateTime=timezone.now(),
|
||||||
|
)
|
||||||
|
except Permission.DoesNotExist:
|
||||||
|
pass
|
||||||
|
return Response({'code': 0, 'msg': '角色保存成功'})
|
||||||
|
|
||||||
|
else:
|
||||||
|
return Response({'code': 400, 'msg': '无效的 action'})
|
||||||
|
|
||||||
|
|
||||||
|
class AddRoleView(APIView):
|
||||||
|
"""
|
||||||
|
添加角色:自动生成角色编码,前端提供角色名称、描述、选择的权限列表
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
role_name = request.data.get('role_name', '').strip()
|
||||||
|
description = request.data.get('description', '').strip()
|
||||||
|
perm_codes = request.data.get('perm_codes', []) # 列表
|
||||||
|
|
||||||
|
if not username or not role_name:
|
||||||
|
return Response({'code': 400, 'msg': '缺少必要参数'})
|
||||||
|
|
||||||
|
# 权限校验
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '000001' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||||||
|
|
||||||
|
# 检查角色名称是否已存在(同俱乐部内唯一)
|
||||||
|
from jituan.services.club_rbac import resolve_role_club_id, role_name_exists
|
||||||
|
target_club_id = resolve_role_club_id(
|
||||||
|
request, (request.data.get('target_club_id') or '').strip() or None,
|
||||||
|
)
|
||||||
|
if role_name_exists(role_name, target_club_id):
|
||||||
|
return Response({'code': 400, 'msg': '该俱乐部下角色名称已存在'})
|
||||||
|
|
||||||
|
# 生成唯一角色编码(格式:ROLE_ + 8位随机字母数字)
|
||||||
|
role_code = f"ROLE_{''.join(random.choices(string.ascii_uppercase + string.digits, k=8))}"
|
||||||
|
|
||||||
|
# 创建角色
|
||||||
|
with transaction.atomic():
|
||||||
|
role = Role.objects.create(
|
||||||
|
RoleUUID=uuid.uuid4().bytes,
|
||||||
|
TenantUUID=b'\x00' * 16,
|
||||||
|
RoleName=role_name,
|
||||||
|
RoleType=2,
|
||||||
|
RoleStatus=1,
|
||||||
|
AssignScope=0,
|
||||||
|
RoleDesc=description,
|
||||||
|
ClubID=target_club_id,
|
||||||
|
CreateTime=timezone.now(),
|
||||||
|
)
|
||||||
|
# 绑定权限(过滤掉不存在的权限编码)
|
||||||
|
for pc in perm_codes:
|
||||||
|
# 不允许添加 000001 权限(此权限仅供超级管理员)
|
||||||
|
if pc == '000001':
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
perm = Permission.objects.get(PermCode=pc)
|
||||||
|
RolePermission.objects.create(
|
||||||
|
RolePermUUID=uuid.uuid4().bytes,
|
||||||
|
RoleUUID=role.RoleUUID,
|
||||||
|
PermUUID=perm.PermUUID,
|
||||||
|
CreateTime=timezone.now(),
|
||||||
|
)
|
||||||
|
except Permission.DoesNotExist:
|
||||||
|
logger.warning(f"添加角色时忽略不存在的权限编码: {pc}")
|
||||||
|
|
||||||
|
return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class GetAdminUserListView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 400, 'msg': '缺少username'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '000001' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||||||
|
|
||||||
|
phone = request.data.get('phone', '').strip()
|
||||||
|
nicheng = request.data.get('nicheng', '').strip()
|
||||||
|
role_code = request.data.get('roleCode', '').strip()
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('pageSize', 20))
|
||||||
|
|
||||||
|
# UserType 是 @property 非数据库字段,不能用 filter;
|
||||||
|
# KefuProfile__isnull=False 已确保只查到客服用户
|
||||||
|
users = User.query.filter(
|
||||||
|
KefuProfile__isnull=False
|
||||||
|
).select_related('KefuProfile')
|
||||||
|
|
||||||
|
from jituan.services.club_rbac import filter_kefu_users_qs
|
||||||
|
users = filter_kefu_users_qs(users, request)
|
||||||
|
|
||||||
|
if phone:
|
||||||
|
users = users.filter(Phone__icontains=phone)
|
||||||
|
if nicheng:
|
||||||
|
users = users.filter(KefuProfile__nicheng__icontains=nicheng)
|
||||||
|
|
||||||
|
# 关键修正:先求值为列表,再传入 phone__in
|
||||||
|
if role_code:
|
||||||
|
target_role = Role.objects.filter(RoleName=role_code).first()
|
||||||
|
if target_role:
|
||||||
|
user_uuids = list(
|
||||||
|
UserRole.objects.filter(RoleUUID=target_role.RoleUUID)
|
||||||
|
.values_list('UserUUID', flat=True)
|
||||||
|
)
|
||||||
|
users = users.filter(UserUUID__in=user_uuids)
|
||||||
|
else:
|
||||||
|
users = users.none()
|
||||||
|
|
||||||
|
users = users.order_by('-UserCreateTime')
|
||||||
|
paginator = Paginator(users, page_size)
|
||||||
|
page_obj = paginator.get_page(page)
|
||||||
|
|
||||||
|
data_list = []
|
||||||
|
for user in page_obj:
|
||||||
|
# 通过 gvsdsdk UserRole 获取用户角色
|
||||||
|
user_role_uuids = list(
|
||||||
|
UserRole.objects.filter(UserUUID=user.UserUUID)
|
||||||
|
.values_list('RoleUUID', flat=True)
|
||||||
|
)
|
||||||
|
user_roles = Role.objects.filter(RoleUUID__in=user_role_uuids)
|
||||||
|
roles_data = [
|
||||||
|
{'role_code': r.RoleName, 'role_name': r.RoleName}
|
||||||
|
for r in user_roles
|
||||||
|
]
|
||||||
|
data_list.append({
|
||||||
|
'phone': user.Phone or '',
|
||||||
|
'yonghuid': user.UserUID or '',
|
||||||
|
'nicheng': user.KefuProfile.nicheng,
|
||||||
|
'club_id': getattr(user, 'ClubID', '') or '',
|
||||||
|
'roles': roles_data,
|
||||||
|
'status': user.KefuProfile.zhuangtai,
|
||||||
|
'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
|
||||||
|
'update_time': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '',
|
||||||
|
'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
|
||||||
|
'UpdateTime': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '',
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'list': data_list,
|
||||||
|
'total': paginator.count,
|
||||||
|
'page': page,
|
||||||
|
'pageSize': page_size
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"GetAdminUserListView 异常: {e}\n{traceback.format_exc()}")
|
||||||
|
return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class GetAdminRolesView(APIView):
|
||||||
|
"""获取所有角色(用于筛选和分配)"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 400, 'msg': '缺少username'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '000001' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||||||
|
|
||||||
|
from jituan.services.club_rbac import filter_roles_for_request
|
||||||
|
roles = filter_roles_for_request(request).values('RoleName', 'ClubID')
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'roles': [
|
||||||
|
{
|
||||||
|
'role_code': r['RoleName'],
|
||||||
|
'role_name': r['RoleName'],
|
||||||
|
'club_id': r.get('ClubID') or '',
|
||||||
|
}
|
||||||
|
for r in roles
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
'''class GetAdminUserListView(APIView):
|
||||||
|
"""获取后台用户列表(支持分页、筛选)"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 400, 'msg': '缺少username'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '000001' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '您无权访问此页面'})
|
||||||
|
|
||||||
|
# 筛选条件
|
||||||
|
phone = request.data.get('phone', '').strip()
|
||||||
|
nicheng = request.data.get('nicheng', '').strip()
|
||||||
|
role_code = request.data.get('roleCode', '').strip()
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('pageSize', 20))
|
||||||
|
|
||||||
|
# 基础查询:存在客服扩展表
|
||||||
|
users = User.query.filter(KefuProfile__isnull=False).select_related('KefuProfile')
|
||||||
|
if phone:
|
||||||
|
users = users.filter(Phone__icontains=phone)
|
||||||
|
if nicheng:
|
||||||
|
users = users.filter(KefuProfile__nicheng__icontains=nicheng)
|
||||||
|
if role_code:
|
||||||
|
# 筛选拥有该角色的用户
|
||||||
|
user_ids_with_role = UserRole.query.filter(role__role_code=role_code).values_list('account_id', flat=True)
|
||||||
|
users = users.filter(phone__in=user_ids_with_role)
|
||||||
|
|
||||||
|
# 排序
|
||||||
|
users = users.order_by('-CreateTime')
|
||||||
|
|
||||||
|
# 分页
|
||||||
|
paginator = Paginator(users, page_size)
|
||||||
|
page_obj = paginator.get_page(page)
|
||||||
|
|
||||||
|
# 构建返回数据
|
||||||
|
data_list = []
|
||||||
|
for user in page_obj:
|
||||||
|
# 获取用户的所有角色
|
||||||
|
user_roles = UserRole.query.filter(account_id=user.Phone).select_related('role')
|
||||||
|
roles_data = [{'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles]
|
||||||
|
data_list.append({
|
||||||
|
'phone': user.Phone,
|
||||||
|
'nicheng': user.KefuProfile.nicheng,
|
||||||
|
'roles': roles_data,
|
||||||
|
'status': user.KefuProfile.zhuangtai,
|
||||||
|
'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'UpdateTime': user.UpdateTime.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'list': data_list,
|
||||||
|
'total': paginator.count,
|
||||||
|
'page': page,
|
||||||
|
'pageSize': page_size
|
||||||
|
}
|
||||||
|
})'''
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ModifyAdminUserView(APIView):
|
||||||
|
"""修改后台用户信息(昵称、密码、二级密码、状态、角色)"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
action = request.data.get('action')
|
||||||
|
if not username or not action:
|
||||||
|
return Response({'code': 400, 'msg': '缺少参数'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '000001' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||||||
|
|
||||||
|
target_yonghuid = request.data.get('yonghuid', '').strip()
|
||||||
|
target_phone = request.data.get('phone', '').strip()
|
||||||
|
if not target_yonghuid and not target_phone:
|
||||||
|
return Response({'code': 400, 'msg': '缺少目标用户(yonghuid 或 phone)'})
|
||||||
|
|
||||||
|
from jituan.services.club_rbac import resolve_kefu_admin_user
|
||||||
|
target_user = resolve_kefu_admin_user(yonghuid=target_yonghuid, phone=target_phone)
|
||||||
|
if not target_user:
|
||||||
|
return Response({'code': 404, 'msg': '用户不存在或非后台账号'})
|
||||||
|
try:
|
||||||
|
target_kefu = target_user.KefuProfile
|
||||||
|
except UserKefu.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '用户扩展信息缺失'})
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
if action == 'update_user_info':
|
||||||
|
# 修改昵称、状态、密码、二级密码
|
||||||
|
nicheng = request.data.get('nicheng')
|
||||||
|
status = request.data.get('status')
|
||||||
|
password = request.data.get('password')
|
||||||
|
erjimima = request.data.get('erjimima')
|
||||||
|
if nicheng is not None:
|
||||||
|
target_kefu.nicheng = nicheng
|
||||||
|
if status is not None:
|
||||||
|
target_kefu.zhuangtai = int(status)
|
||||||
|
if password:
|
||||||
|
if len(password) < 6:
|
||||||
|
return Response({'code': 400, 'msg': '密码至少6位'})
|
||||||
|
target_user.SetPassword(password)
|
||||||
|
if erjimima:
|
||||||
|
if len(erjimima) < 6:
|
||||||
|
return Response({'code': 400, 'msg': '二级密码至少6位'})
|
||||||
|
import bcrypt as _bcrypt
|
||||||
|
target_kefu.erjimima = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
|
||||||
|
target_user.save()
|
||||||
|
target_kefu.save()
|
||||||
|
return Response({'code': 0, 'msg': '修改成功'})
|
||||||
|
|
||||||
|
elif action == 'add_user_roles':
|
||||||
|
role_codes = request.data.get('role_codes', [])
|
||||||
|
if not role_codes:
|
||||||
|
return Response({'code': 400, 'msg': '请选择角色'})
|
||||||
|
from jituan.services.club_rbac import resolve_role_by_name
|
||||||
|
club_id = getattr(target_user, 'ClubID', '') or ''
|
||||||
|
added, missing = [], []
|
||||||
|
for rc in role_codes:
|
||||||
|
role = resolve_role_by_name(rc, club_id)
|
||||||
|
if not role:
|
||||||
|
missing.append(rc)
|
||||||
|
continue
|
||||||
|
_, created = UserRole.objects.get_or_create(
|
||||||
|
UserUUID=target_user.UserUUID,
|
||||||
|
RoleUUID=role.RoleUUID,
|
||||||
|
defaults={
|
||||||
|
'UserRoleUUID': uuid.uuid4().bytes,
|
||||||
|
'CompanyUUID': uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
added.append(role.RoleName)
|
||||||
|
elif role.RoleName not in added:
|
||||||
|
added.append(role.RoleName)
|
||||||
|
if not added:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'msg': f'未找到可绑定的角色:{", ".join(missing or role_codes)}。请确认顶栏俱乐部与角色所属俱乐部一致',
|
||||||
|
})
|
||||||
|
if missing:
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': f'已绑定:{", ".join(added)};未找到:{", ".join(missing)}',
|
||||||
|
})
|
||||||
|
return Response({'code': 0, 'msg': '角色添加成功'})
|
||||||
|
|
||||||
|
elif action == 'remove_user_role':
|
||||||
|
role_code = request.data.get('role_code')
|
||||||
|
if not role_code:
|
||||||
|
return Response({'code': 400, 'msg': '缺少角色编码'})
|
||||||
|
from jituan.services.club_rbac import resolve_role_by_name
|
||||||
|
club_id = getattr(target_user, 'ClubID', '') or ''
|
||||||
|
role = resolve_role_by_name(role_code, club_id)
|
||||||
|
if not role:
|
||||||
|
return Response({'code': 404, 'msg': '角色不存在或与用户俱乐部不匹配'})
|
||||||
|
UserRole.objects.filter(UserUUID=target_user.UserUUID, RoleUUID=role.RoleUUID).delete()
|
||||||
|
return Response({'code': 0, 'msg': '角色移除成功'})
|
||||||
|
|
||||||
|
else:
|
||||||
|
return Response({'code': 400, 'msg': '无效的action'})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AddAdminUserView(APIView):
|
||||||
|
"""添加后台用户(客服账号)"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 400, 'msg': '缺少username'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
if '000001' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '您无权进行此操作'})
|
||||||
|
|
||||||
|
phone = request.data.get('phone', '').strip()
|
||||||
|
password = request.data.get('password', '').strip()
|
||||||
|
erjimima = request.data.get('erjimima', '').strip()
|
||||||
|
nicheng = request.data.get('nicheng', '').strip()
|
||||||
|
role_codes = request.data.get('role_codes', [])
|
||||||
|
|
||||||
|
if not all([phone, password, erjimima, nicheng]):
|
||||||
|
return Response({'code': 400, 'msg': '请填写完整信息'})
|
||||||
|
if len(password) < 6 or len(erjimima) < 6:
|
||||||
|
return Response({'code': 400, 'msg': '密码和二级密码至少6位'})
|
||||||
|
|
||||||
|
from jituan.services.club_context import resolve_club_id_from_request
|
||||||
|
from jituan.services.club_rbac import resolve_roles_for_binding
|
||||||
|
|
||||||
|
target_club_id = resolve_club_id_from_request(request)
|
||||||
|
|
||||||
|
# 校验手机号是否已被后台客服占用(同俱乐部优先)
|
||||||
|
existing_kefu = User.query.filter(
|
||||||
|
Phone=phone, KefuProfile__isnull=False
|
||||||
|
).order_by('-UserCreateTime').first()
|
||||||
|
if existing_kefu:
|
||||||
|
return Response({'code': 400, 'msg': '该账号已存在'})
|
||||||
|
|
||||||
|
if not isinstance(role_codes, list):
|
||||||
|
return Response({'code': 400, 'msg': 'role_codes 格式错误'})
|
||||||
|
|
||||||
|
# 分配角色(默认客服 + 所选角色,去重;任一角色找不到则整单拒绝)
|
||||||
|
role_names_to_bind = []
|
||||||
|
seen_names = set()
|
||||||
|
for name in (['客服'] if '客服' not in role_codes else []) + list(role_codes):
|
||||||
|
n = (name or '').strip()
|
||||||
|
if n and n not in seen_names:
|
||||||
|
seen_names.add(n)
|
||||||
|
role_names_to_bind.append(n)
|
||||||
|
|
||||||
|
roles_to_bind, missing_roles = resolve_roles_for_binding(role_names_to_bind, target_club_id)
|
||||||
|
if missing_roles:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'msg': f'角色不存在或与当前俱乐部不匹配:{", ".join(missing_roles)}',
|
||||||
|
})
|
||||||
|
if not roles_to_bind:
|
||||||
|
return Response({'code': 400, 'msg': '请至少选择一个有效角色'})
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
# 事务内二次校验,防止并发重复创建
|
||||||
|
if User.query.filter(Phone=phone, KefuProfile__isnull=False).exists():
|
||||||
|
return Response({'code': 400, 'msg': '该账号已存在'})
|
||||||
|
|
||||||
|
# 生成唯一的 yonghuid(7位数字)
|
||||||
|
while True:
|
||||||
|
yonghuid = str(random.randint(1000000, 9999999))
|
||||||
|
if not User.query.filter(UserUID=yonghuid).exists():
|
||||||
|
break
|
||||||
|
|
||||||
|
# 生成唯一的 openid(格式:admin_{时间戳}_{随机6位})
|
||||||
|
timestamp = str(int(time.time() * 1000))
|
||||||
|
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||||
|
openid = f"admin_{timestamp}_{random_suffix}"
|
||||||
|
while User.query.filter(OpenID=openid).exists():
|
||||||
|
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||||
|
openid = f"admin_{timestamp}_{random_suffix}"
|
||||||
|
|
||||||
|
user_main = User.query.create(
|
||||||
|
UserUID=yonghuid,
|
||||||
|
UserName=openid,
|
||||||
|
OpenID=openid,
|
||||||
|
Phone=phone,
|
||||||
|
ClubID=target_club_id,
|
||||||
|
)
|
||||||
|
user_main.SetPassword(password)
|
||||||
|
user_main.save(update_fields=['UserPassword', 'ClubID'])
|
||||||
|
# 创建客服扩展表(后台用户扩展)
|
||||||
|
import bcrypt as _bcrypt
|
||||||
|
erjimima_hashed = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
|
||||||
|
UserKefu.query.create(
|
||||||
|
user=user_main,
|
||||||
|
nicheng=nicheng,
|
||||||
|
erjimima=erjimima_hashed,
|
||||||
|
zhuangtai=1
|
||||||
|
)
|
||||||
|
for role in roles_to_bind:
|
||||||
|
UserRole.objects.get_or_create(
|
||||||
|
UserUUID=user_main.UserUUID,
|
||||||
|
RoleUUID=role.RoleUUID,
|
||||||
|
defaults={
|
||||||
|
'UserRoleUUID': uuid.uuid4().bytes,
|
||||||
|
'CompanyUUID': uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response({'code': 0, 'msg': '添加成功'})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
354
backend/views/sections.py
Normal file
354
backend/views/sections.py
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
import hmac
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import xmltodict
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from calendar import monthrange
|
||||||
|
from decimal import Decimal
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# 工具类导入
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action,
|
||||||
|
update_shangjia_daily
|
||||||
|
)
|
||||||
|
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||||
|
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||||
|
from jituan.services.club_penalty import (
|
||||||
|
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||||
|
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||||
|
)
|
||||||
|
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
from ..utils import (
|
||||||
|
verify_kefu_permission,
|
||||||
|
PERMISSION_TO_SHENFEN,
|
||||||
|
SHENFEN_PROFILE_MAP,
|
||||||
|
has_fadan_view_permission,
|
||||||
|
has_merchant_order_permission,
|
||||||
|
check_fadan_permission,
|
||||||
|
pick_order_id,
|
||||||
|
pick_penalty_create_params,
|
||||||
|
write_xiugai_log,
|
||||||
|
XIUGAI_LEIXING_DASHOU,
|
||||||
|
XIUGAI_LEIXING_GUANSHI,
|
||||||
|
XIUGAI_LEIXING_SHANGJIA,
|
||||||
|
XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
XIUGAI_LEIXING_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# models 集中导入
|
||||||
|
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||||
|
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||||
|
## backend
|
||||||
|
from backend.models import WithdrawalDailyStats
|
||||||
|
|
||||||
|
## users
|
||||||
|
from users.models import (
|
||||||
|
UserGuanshi, UserBoss, UserZuzhang,
|
||||||
|
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
## orders
|
||||||
|
from orders.models import (
|
||||||
|
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||||
|
OrderPlayerHistory, Order, RefundRecord,
|
||||||
|
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||||
|
)
|
||||||
|
|
||||||
|
## shop
|
||||||
|
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||||
|
|
||||||
|
## products
|
||||||
|
from products.models import (
|
||||||
|
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||||
|
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||||
|
)
|
||||||
|
|
||||||
|
## config
|
||||||
|
from config.models import (
|
||||||
|
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||||
|
TixianQuotaDefault,
|
||||||
|
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||||
|
)
|
||||||
|
|
||||||
|
## rank
|
||||||
|
from rank.models import (
|
||||||
|
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||||
|
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||||
|
)
|
||||||
|
|
||||||
|
# 序列化器
|
||||||
|
from ..serializers import PopupPageSerializer
|
||||||
|
|
||||||
|
# 全局常量、日志对象
|
||||||
|
logger = logging.getLogger('houtai')
|
||||||
|
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||||
|
|
||||||
|
class HqbkxxView(APIView):
|
||||||
|
"""
|
||||||
|
获取板块配置所需全部数据
|
||||||
|
POST /houtai/hqbkxx
|
||||||
|
Body: { "phone": "管理员手机号" }
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# 1. 从前端获取传递的账号,用于防越权校验
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
|
||||||
|
# 2. 调用公共验证方法
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
|
||||||
|
# 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
# ---------- 验证通过 ----------
|
||||||
|
# 4. 检查是否有板块管理权限
|
||||||
|
if 'bankuai' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
|
||||||
|
scope_err = block_non_group_catalog_scope(request)
|
||||||
|
if scope_err:
|
||||||
|
return Response({'code': 400, 'msg': scope_err})
|
||||||
|
|
||||||
|
# 5. 查询所有板块
|
||||||
|
bankuai_list = []
|
||||||
|
for bk in Bankuai.query.all():
|
||||||
|
bankuai_list.append({
|
||||||
|
'bankuai_id': bk.bankuai_id,
|
||||||
|
'mingcheng': bk.mingcheng,
|
||||||
|
'CreateTime': bk.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if bk.CreateTime else ''
|
||||||
|
})
|
||||||
|
|
||||||
|
# 6. 查询所有商品类型
|
||||||
|
shangpin_leixing_list = []
|
||||||
|
for sp in ShangpinLeixing.query.all():
|
||||||
|
shangpin_leixing_list.append({
|
||||||
|
'id': sp.id,
|
||||||
|
'jieshao': sp.jieshao,
|
||||||
|
'tupian_url': sp.tupian_url or '',
|
||||||
|
'bankuai_id': sp.bankuai_id
|
||||||
|
})
|
||||||
|
|
||||||
|
# 7. 查询会员(附带当前俱乐部售价)
|
||||||
|
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL
|
||||||
|
from jituan.models import ClubHuiyuanPrice
|
||||||
|
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
|
||||||
|
|
||||||
|
scope = resolve_club_scope(request)
|
||||||
|
club_id = resolve_club_id_from_request(request)
|
||||||
|
price_map = {}
|
||||||
|
if scope != DATA_SCOPE_ALL:
|
||||||
|
for row in ClubHuiyuanPrice.query.filter(club_id=club_id):
|
||||||
|
price_map[row.huiyuan_id] = row
|
||||||
|
|
||||||
|
huiyuan_list = []
|
||||||
|
for hy in Huiyuan.query.all():
|
||||||
|
row = price_map.get(hy.huiyuan_id)
|
||||||
|
if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT and row is None:
|
||||||
|
continue
|
||||||
|
if row is not None and not row.is_enabled:
|
||||||
|
continue
|
||||||
|
item = {
|
||||||
|
'huiyuan_id': hy.huiyuan_id,
|
||||||
|
'jieshao': hy.jieshao,
|
||||||
|
'bankuai_id': hy.bankuai_id,
|
||||||
|
'jiage': str(row.jiage) if row else str(hy.jiage),
|
||||||
|
'guanshifc': str(row.guanshifc) if row else str(hy.guanshifc),
|
||||||
|
'zuzhangfc': str(row.zuzhangfc) if row else str(hy.zuzhangfc),
|
||||||
|
}
|
||||||
|
huiyuan_list.append(item)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'ok',
|
||||||
|
'data': {
|
||||||
|
'bankuai': bankuai_list,
|
||||||
|
'shangpin_leixing': shangpin_leixing_list,
|
||||||
|
'huiyuan': huiyuan_list,
|
||||||
|
'club_id': club_id,
|
||||||
|
'scope': scope,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class BkxgView(APIView):
|
||||||
|
"""
|
||||||
|
板块增删改统一接口
|
||||||
|
POST /houtai/bkxg
|
||||||
|
Body: {
|
||||||
|
"phone": "管理员手机号",
|
||||||
|
"action": "create | update | delete | add_items | remove_items",
|
||||||
|
"bankuai_id": int,
|
||||||
|
"bankuai_name": str,
|
||||||
|
"item_type": "shangpin_leixing | huiyuan",
|
||||||
|
"item_ids": [1,2,3]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# 1. 从前端获取传递的账号
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
|
||||||
|
# 2. 调用公共验证方法
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
|
||||||
|
# 3. 验证失败直接返回
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
# 4. 检查板块管理权限
|
||||||
|
if 'bankuai' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
|
||||||
|
scope_err = block_non_group_catalog_scope(request)
|
||||||
|
if scope_err:
|
||||||
|
return Response({'code': 400, 'msg': scope_err})
|
||||||
|
|
||||||
|
# 5. 获取操作类型
|
||||||
|
action = request.data.get('action')
|
||||||
|
if not action:
|
||||||
|
return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
|
||||||
|
|
||||||
|
# ==================== 创建板块 ====================
|
||||||
|
if action == 'create':
|
||||||
|
bankuai_name = request.data.get('bankuai_name', '').strip()
|
||||||
|
if not bankuai_name:
|
||||||
|
return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
|
||||||
|
if Bankuai.query.filter(mingcheng=bankuai_name).exists():
|
||||||
|
return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
|
||||||
|
|
||||||
|
shangpin_ids = request.data.get('shangpin_ids', [])
|
||||||
|
huiyuan_ids = request.data.get('huiyuan_ids', [])
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
new_bk = Bankuai.query.create(mingcheng=bankuai_name)
|
||||||
|
if shangpin_ids:
|
||||||
|
ShangpinLeixing.query.filter(
|
||||||
|
id__in=shangpin_ids,
|
||||||
|
bankuai__isnull=True
|
||||||
|
).update(bankuai=new_bk)
|
||||||
|
if huiyuan_ids:
|
||||||
|
Huiyuan.query.filter(
|
||||||
|
huiyuan_id__in=huiyuan_ids,
|
||||||
|
bankuai__isnull=True
|
||||||
|
).update(bankuai=new_bk)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': '创建成功',
|
||||||
|
'data': {'bankuai_id': new_bk.bankuai_id}
|
||||||
|
})
|
||||||
|
|
||||||
|
# ==================== 以下操作需要 bankuai_id ====================
|
||||||
|
bankuai_id = request.data.get('bankuai_id')
|
||||||
|
if not bankuai_id:
|
||||||
|
return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
bk = Bankuai.query.get(bankuai_id=bankuai_id)
|
||||||
|
except Bankuai.DoesNotExist:
|
||||||
|
return Response({'code': 1, 'msg': '板块不存在'}, status=404)
|
||||||
|
|
||||||
|
# ==================== 修改板块名称 ====================
|
||||||
|
if action == 'update':
|
||||||
|
new_name = request.data.get('bankuai_name', '').strip()
|
||||||
|
if not new_name:
|
||||||
|
return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
|
||||||
|
if Bankuai.query.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists():
|
||||||
|
return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
|
||||||
|
bk.mingcheng = new_name
|
||||||
|
bk.save()
|
||||||
|
return Response({'code': 0, 'msg': '修改成功'})
|
||||||
|
|
||||||
|
# ==================== 删除板块 ====================
|
||||||
|
elif action == 'delete':
|
||||||
|
with transaction.atomic():
|
||||||
|
ShangpinLeixing.query.filter(bankuai=bk).update(bankuai=None)
|
||||||
|
Huiyuan.query.filter(bankuai=bk).update(bankuai=None)
|
||||||
|
bk.delete()
|
||||||
|
return Response({'code': 0, 'msg': '删除成功'})
|
||||||
|
|
||||||
|
# ==================== 添加绑定 ====================
|
||||||
|
elif action == 'add_items':
|
||||||
|
item_type = request.data.get('item_type')
|
||||||
|
item_ids = request.data.get('item_ids', [])
|
||||||
|
if not item_type or not item_ids:
|
||||||
|
return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
|
||||||
|
|
||||||
|
if item_type == 'shangpin_leixing':
|
||||||
|
ShangpinLeixing.query.filter(
|
||||||
|
id__in=item_ids,
|
||||||
|
bankuai__isnull=True
|
||||||
|
).update(bankuai=bk)
|
||||||
|
elif item_type == 'huiyuan':
|
||||||
|
Huiyuan.query.filter(
|
||||||
|
huiyuan_id__in=item_ids,
|
||||||
|
bankuai__isnull=True
|
||||||
|
).update(bankuai=bk)
|
||||||
|
else:
|
||||||
|
return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
|
||||||
|
return Response({'code': 0, 'msg': '添加成功'})
|
||||||
|
|
||||||
|
# ==================== 移除绑定 ====================
|
||||||
|
elif action == 'remove_items':
|
||||||
|
item_type = request.data.get('item_type')
|
||||||
|
item_ids = request.data.get('item_ids', [])
|
||||||
|
if not item_type or not item_ids:
|
||||||
|
return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
|
||||||
|
|
||||||
|
if item_type == 'shangpin_leixing':
|
||||||
|
ShangpinLeixing.query.filter(
|
||||||
|
id__in=item_ids,
|
||||||
|
bankuai=bk
|
||||||
|
).update(bankuai=None)
|
||||||
|
elif item_type == 'huiyuan':
|
||||||
|
Huiyuan.query.filter(
|
||||||
|
huiyuan_id__in=item_ids,
|
||||||
|
bankuai=bk
|
||||||
|
).update(bankuai=None)
|
||||||
|
else:
|
||||||
|
return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
|
||||||
|
return Response({'code': 0, 'msg': '移除成功'})
|
||||||
|
|
||||||
|
else:
|
||||||
|
return Response({'code': 1, 'msg': '未知操作'}, status=400)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
933
backend/views/shops.py
Normal file
933
backend/views/shops.py
Normal file
@@ -0,0 +1,933 @@
|
|||||||
|
import hmac
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import xmltodict
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from calendar import monthrange
|
||||||
|
from decimal import Decimal
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# 工具类导入
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action,
|
||||||
|
update_shangjia_daily
|
||||||
|
)
|
||||||
|
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||||
|
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||||
|
from jituan.services.club_penalty import (
|
||||||
|
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||||
|
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||||
|
)
|
||||||
|
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
from ..utils import (
|
||||||
|
verify_kefu_permission,
|
||||||
|
PERMISSION_TO_SHENFEN,
|
||||||
|
SHENFEN_PROFILE_MAP,
|
||||||
|
has_fadan_view_permission,
|
||||||
|
has_merchant_order_permission,
|
||||||
|
check_fadan_permission,
|
||||||
|
pick_order_id,
|
||||||
|
pick_penalty_create_params,
|
||||||
|
write_xiugai_log,
|
||||||
|
XIUGAI_LEIXING_DASHOU,
|
||||||
|
XIUGAI_LEIXING_GUANSHI,
|
||||||
|
XIUGAI_LEIXING_SHANGJIA,
|
||||||
|
XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
XIUGAI_LEIXING_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# models 集中导入
|
||||||
|
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||||
|
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||||
|
## backend
|
||||||
|
from backend.models import WithdrawalDailyStats
|
||||||
|
|
||||||
|
## users
|
||||||
|
from users.models import (
|
||||||
|
UserGuanshi, UserBoss, UserZuzhang,
|
||||||
|
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
## orders
|
||||||
|
from orders.models import (
|
||||||
|
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||||
|
OrderPlayerHistory, Order, RefundRecord,
|
||||||
|
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||||
|
)
|
||||||
|
|
||||||
|
## shop
|
||||||
|
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||||
|
|
||||||
|
## products
|
||||||
|
from products.models import (
|
||||||
|
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||||
|
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||||
|
)
|
||||||
|
|
||||||
|
## config
|
||||||
|
from config.models import (
|
||||||
|
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||||
|
TixianQuotaDefault,
|
||||||
|
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||||
|
)
|
||||||
|
|
||||||
|
## rank
|
||||||
|
from rank.models import (
|
||||||
|
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||||
|
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||||
|
)
|
||||||
|
|
||||||
|
# 序列化器
|
||||||
|
from ..serializers import PopupPageSerializer
|
||||||
|
|
||||||
|
# 全局常量、日志对象
|
||||||
|
logger = logging.getLogger('houtai')
|
||||||
|
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||||
|
|
||||||
|
class ShopListView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# 1. 调用公共权限验证(与您给的示例完全一致)
|
||||||
|
username_frontend = request.data.get('username')
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username_frontend)
|
||||||
|
if kefu is None:
|
||||||
|
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||||||
|
|
||||||
|
# 2. 检查是否拥有所需权限(999a~999e 任意一个)
|
||||||
|
required_perms = {'999a', '999b', '999c', '999d', '999e'}
|
||||||
|
if not required_perms.intersection(set(permissions)):
|
||||||
|
return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'})
|
||||||
|
|
||||||
|
# 3. 解析请求参数
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('page_size', 10))
|
||||||
|
dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip()
|
||||||
|
zhuangtai = request.data.get('zhuangtai')
|
||||||
|
lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip()
|
||||||
|
user_id = request.data.get('user_id', '').strip()
|
||||||
|
ketixian_yue_min = request.data.get('ketixian_yue_min')
|
||||||
|
ketixian_yue_max = request.data.get('ketixian_yue_max')
|
||||||
|
zhengqu_zonge_min = request.data.get('zhengqu_zonge_min')
|
||||||
|
zhengqu_zonge_max = request.data.get('zhengqu_zonge_max')
|
||||||
|
|
||||||
|
# 4. 构建查询集(先查店铺,预加载关联凭证)
|
||||||
|
queryset = Dianpu.query.select_related('pingzheng').all()
|
||||||
|
|
||||||
|
if dianpu_mingcheng:
|
||||||
|
queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng)
|
||||||
|
if zhuangtai is not None:
|
||||||
|
queryset = queryset.filter(zhuangtai=zhuangtai)
|
||||||
|
if lianxi_dianhua:
|
||||||
|
queryset = queryset.filter(lianxi_dianhua__icontains=lianxi_dianhua)
|
||||||
|
if user_id:
|
||||||
|
queryset = queryset.filter(pingzheng__yonghu=user_id)
|
||||||
|
|
||||||
|
# 金额范围过滤
|
||||||
|
if ketixian_yue_min is not None:
|
||||||
|
queryset = queryset.filter(ketixian_yue__gte=ketixian_yue_min)
|
||||||
|
if ketixian_yue_max is not None:
|
||||||
|
queryset = queryset.filter(ketixian_yue__lte=ketixian_yue_max)
|
||||||
|
if zhengqu_zonge_min is not None:
|
||||||
|
queryset = queryset.filter(zhengqu_zonge__gte=zhengqu_zonge_min)
|
||||||
|
if zhengqu_zonge_max is not None:
|
||||||
|
queryset = queryset.filter(zhengqu_zonge__lte=zhengqu_zonge_max)
|
||||||
|
|
||||||
|
# 5. 分页
|
||||||
|
paginator = Paginator(queryset, page_size)
|
||||||
|
page_obj = paginator.get_page(page)
|
||||||
|
|
||||||
|
# 6. 构造返回数据
|
||||||
|
shop_list = []
|
||||||
|
for dianpu in page_obj:
|
||||||
|
pingzheng = dianpu.pingzheng
|
||||||
|
shop_list.append({
|
||||||
|
'id': dianpu.id,
|
||||||
|
'dianpu_mingcheng': dianpu.dianpu_mingcheng,
|
||||||
|
'dianpu_touxiang': dianpu.dianpu_touxiang or '',
|
||||||
|
'lianxi_dianhua': dianpu.lianxi_dianhua or '',
|
||||||
|
'weixinhao': dianpu.weixinhao or '',
|
||||||
|
'zhuangtai': dianpu.zhuangtai,
|
||||||
|
'bangding_yonghushu': dianpu.bangding_yonghushu,
|
||||||
|
'erweima_url': dianpu.erweima_url or '',
|
||||||
|
'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng,
|
||||||
|
'shouyi_choucheng_feilv': dianpu.shouyi_choucheng_feilv or 0,
|
||||||
|
'ketixian_yue': dianpu.ketixian_yue,
|
||||||
|
'zhengqu_zonge': dianpu.zhengqu_zonge,
|
||||||
|
'meiri_tixian_xiane': dianpu.meiri_tixian_xiane or 0,
|
||||||
|
'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane,
|
||||||
|
'pingzheng__yonghu': pingzheng.yonghu, # 用户ID
|
||||||
|
'pingzheng__zhanghao': pingzheng.zhanghao, # 登录账号
|
||||||
|
})
|
||||||
|
|
||||||
|
# 获取默认配置(ID=1,不存在则创建)
|
||||||
|
default_config, _ = DianpuMorenPeizhi.query.get_or_create(id=1, defaults={
|
||||||
|
'moren_choucheng_feilv': 0.1,
|
||||||
|
'moren_tixian_xiane': 1000.00
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'success',
|
||||||
|
'data': {
|
||||||
|
'list': shop_list,
|
||||||
|
'total': paginator.count,
|
||||||
|
'default_config': {
|
||||||
|
'moren_choucheng_feilv': default_config.moren_choucheng_feilv,
|
||||||
|
'moren_tixian_xiane': default_config.moren_tixian_xiane,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class ShopModifyView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# 1. 身份验证
|
||||||
|
username_frontend = request.data.get('username')
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username_frontend)
|
||||||
|
if kefu is None:
|
||||||
|
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||||||
|
|
||||||
|
# 转换权限为集合方便判断
|
||||||
|
user_perms = set(permissions)
|
||||||
|
action = request.data.get('action', '')
|
||||||
|
|
||||||
|
# 2. 根据 action 分派任务
|
||||||
|
if action == 'update_dianpu':
|
||||||
|
return self._update_dianpu(request, user_perms)
|
||||||
|
elif action == 'add_dianpu':
|
||||||
|
return self._add_dianpu(request, user_perms)
|
||||||
|
elif action == 'update_default':
|
||||||
|
return self._update_default(request, user_perms)
|
||||||
|
else:
|
||||||
|
return Response({'code': 400, 'msg': '未知操作类型'})
|
||||||
|
|
||||||
|
def _update_dianpu(self, request, user_perms):
|
||||||
|
"""修改店铺信息,精确权限控制"""
|
||||||
|
dianpu_id = request.data.get('dianpu_id')
|
||||||
|
if not dianpu_id:
|
||||||
|
return Response({'code': 400, 'msg': '缺少店铺ID'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id)
|
||||||
|
|
||||||
|
# 基础信息修改 (999e)
|
||||||
|
if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']):
|
||||||
|
if '999e' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改店铺基本信息(999e)'})
|
||||||
|
if 'dianpu_mingcheng' in request.data:
|
||||||
|
dianpu.dianpu_mingcheng = request.data['dianpu_mingcheng']
|
||||||
|
if 'lianxi_dianhua' in request.data:
|
||||||
|
dianpu.lianxi_dianhua = request.data['lianxi_dianhua']
|
||||||
|
if 'weixinhao' in request.data:
|
||||||
|
dianpu.weixinhao = request.data['weixinhao']
|
||||||
|
|
||||||
|
# 店铺状态 (999a)
|
||||||
|
if 'zhuangtai' in request.data:
|
||||||
|
if '999a' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改店铺状态(999a)'})
|
||||||
|
new_status = int(request.data['zhuangtai'])
|
||||||
|
if new_status not in (0, 1):
|
||||||
|
return Response({'code': 400, 'msg': '店铺状态无效'})
|
||||||
|
dianpu.zhuangtai = new_status
|
||||||
|
|
||||||
|
# 可提现余额 (999b)
|
||||||
|
if 'ketixian_yue' in request.data:
|
||||||
|
if '999b' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改可提现余额(999b)'})
|
||||||
|
dianpu.ketixian_yue = request.data['ketixian_yue']
|
||||||
|
|
||||||
|
# 提现限额相关 (999c)
|
||||||
|
if 'kaiqi_meiri_xiane' in request.data or 'meiri_tixian_xiane' in request.data:
|
||||||
|
if '999c' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改提现限额配置(999c)'})
|
||||||
|
if 'kaiqi_meiri_xiane' in request.data:
|
||||||
|
dianpu.kaiqi_meiri_xiane = request.data['kaiqi_meiri_xiane']
|
||||||
|
if 'meiri_tixian_xiane' in request.data:
|
||||||
|
dianpu.meiri_tixian_xiane = request.data['meiri_tixian_xiane']
|
||||||
|
|
||||||
|
# 抽成相关 (999d)
|
||||||
|
if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data:
|
||||||
|
if '999d' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改抽成配置(999d)'})
|
||||||
|
if 'kaiqi_shouyi_choucheng' in request.data:
|
||||||
|
dianpu.kaiqi_shouyi_choucheng = request.data['kaiqi_shouyi_choucheng']
|
||||||
|
if 'shouyi_choucheng_feilv' in request.data:
|
||||||
|
dianpu.shouyi_choucheng_feilv = request.data['shouyi_choucheng_feilv']
|
||||||
|
|
||||||
|
dianpu.save() # Django 会自动判断更新哪些字段
|
||||||
|
return Response({'code': 0, 'msg': '店铺信息更新成功'})
|
||||||
|
|
||||||
|
except Dianpu.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '店铺不存在'})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("修改店铺失败")
|
||||||
|
return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
|
||||||
|
|
||||||
|
def _add_dianpu(self, request, user_perms):
|
||||||
|
"""添加店铺,需要权限 999b"""
|
||||||
|
if '999b' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限添加店铺(999b)'})
|
||||||
|
|
||||||
|
yonghu_id = request.data.get('yonghu_id')
|
||||||
|
zhanghao = request.data.get('zhanghao')
|
||||||
|
mima = request.data.get('mima')
|
||||||
|
dianpu_mingcheng = request.data.get('dianpu_mingcheng')
|
||||||
|
|
||||||
|
if not all([yonghu_id, zhanghao, mima, dianpu_mingcheng]):
|
||||||
|
return Response({'code': 400, 'msg': '必填字段缺失(用户ID、账号、密码、店铺名称)'})
|
||||||
|
|
||||||
|
# 检查用户是否存在
|
||||||
|
if not User.query.filter(UserUID=yonghu_id).exists():
|
||||||
|
return Response({'code': 404, 'msg': '用户不存在'})
|
||||||
|
|
||||||
|
# 检查凭证是否已存在
|
||||||
|
if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists():
|
||||||
|
return Response({'code': 400, 'msg': '登录账号已存在'})
|
||||||
|
if YonghuPingzheng.query.filter(yonghu=yonghu_id).exists():
|
||||||
|
return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
pingzheng = YonghuPingzheng.query.create(
|
||||||
|
yonghu=yonghu_id,
|
||||||
|
zhanghao=zhanghao,
|
||||||
|
mima=mima,
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
dianpu = Dianpu.query.create(
|
||||||
|
pingzheng=pingzheng,
|
||||||
|
dianpu_mingcheng=dianpu_mingcheng,
|
||||||
|
lianxi_dianhua=request.data.get('lianxi_dianhua', ''),
|
||||||
|
weixinhao=request.data.get('weixinhao', ''),
|
||||||
|
kaiqi_shouyi_choucheng=request.data.get('kaiqi_shouyi_choucheng', False),
|
||||||
|
shouyi_choucheng_feilv=request.data.get('shouyi_choucheng_feilv', 0),
|
||||||
|
kaiqi_meiri_xiane=request.data.get('kaiqi_meiri_xiane', False),
|
||||||
|
meiri_tixian_xiane=request.data.get('meiri_tixian_xiane', 0),
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '店铺创建成功', 'dianpu_id': dianpu.id})
|
||||||
|
except IntegrityError as e:
|
||||||
|
logger.error("创建店铺违反唯一性约束: %s", e)
|
||||||
|
return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("创建店铺失败")
|
||||||
|
return Response({'code': 500, 'msg': '创建失败,请稍后重试'})
|
||||||
|
|
||||||
|
def _update_default(self, request, user_perms):
|
||||||
|
"""修改默认配置,按修改内容校验权限"""
|
||||||
|
feilv = request.data.get('moren_choucheng_feilv')
|
||||||
|
xiane = request.data.get('moren_tixian_xiane')
|
||||||
|
modify_commission = feilv is not None
|
||||||
|
modify_withdraw = xiane is not None
|
||||||
|
|
||||||
|
if modify_commission and '999d' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'})
|
||||||
|
if modify_withdraw and '999c' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
config, _ = DianpuMorenPeizhi.query.get_or_create(id=1)
|
||||||
|
if modify_commission:
|
||||||
|
config.moren_choucheng_feilv = feilv
|
||||||
|
if modify_withdraw:
|
||||||
|
config.moren_tixian_xiane = xiane
|
||||||
|
config.save(update_fields=(['moren_choucheng_feilv'] if modify_commission else []) +
|
||||||
|
(['moren_tixian_xiane'] if modify_withdraw else []))
|
||||||
|
return Response({'code': 0, 'msg': '默认配置更新成功'})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("更新默认配置失败")
|
||||||
|
return Response({'code': 500, 'msg': '更新失败'})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 1. 公共商品类型 + 审核模式 ====================
|
||||||
|
class ShopPublicTypeAndAuditView(APIView):
|
||||||
|
"""
|
||||||
|
返回:①正常状态的公共商品类型列表 ②全局商品审核模式开关
|
||||||
|
前端请求路径:/houtai/htdphqsplx
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# ---------- 身份与权限校验 ----------
|
||||||
|
username = request.data.get('username')
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||||||
|
|
||||||
|
# 拥有 1199ab/1199abc/1199abd 任一个即可访问
|
||||||
|
if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
|
||||||
|
return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'})
|
||||||
|
|
||||||
|
# ---------- 查询公共商品类型(仅正常审核状态的) ----------
|
||||||
|
try:
|
||||||
|
# shenhezhuangtai = 1 表示正常 (根据模型默认值)
|
||||||
|
types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values(
|
||||||
|
'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin'
|
||||||
|
)
|
||||||
|
type_list = []
|
||||||
|
for t in types_qs:
|
||||||
|
type_list.append({
|
||||||
|
'id': t['id'],
|
||||||
|
'jieshao': t['jieshao'] or '',
|
||||||
|
'tupian_url': t['tupian_url'] or '',
|
||||||
|
'yaoqiuleixing': t['yaoqiuleixing'],
|
||||||
|
'huiyuan_id': t['huiyuan_id'],
|
||||||
|
'yongjin': t['yongjin'],
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("查询公共商品类型失败")
|
||||||
|
return Response({'code': 500, 'msg': '服务器错误,请稍后重试'})
|
||||||
|
|
||||||
|
# ---------- 获取审核模式配置 ----------
|
||||||
|
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
|
||||||
|
kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'public_types': type_list,
|
||||||
|
'kaiqi_shenhe': kaiqi_shenhe
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 2. 店铺列表(支持搜索、筛选、分页) ====================
|
||||||
|
class ShopListForProductView(APIView):
|
||||||
|
"""
|
||||||
|
返回店铺列表,用于商品管理页面选择店铺。
|
||||||
|
支持:关键词搜索(店铺ID/名称/用户ID)、店铺状态筛选、分页。
|
||||||
|
前端请求路径:/houtai/htdphqdpys
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username')
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return Response({'code': 403, 'msg': '身份验证失败'})
|
||||||
|
if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
|
||||||
|
return Response({'code': 403, 'msg': '无权访问店铺列表'})
|
||||||
|
|
||||||
|
# ---------- 安全解析分页参数 ----------
|
||||||
|
try:
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('page_size', 10))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return Response({'code': 400, 'msg': '分页参数格式错误'})
|
||||||
|
|
||||||
|
# 搜索关键词(字符串,ORM 自动防注入)
|
||||||
|
keyword = str(request.data.get('keyword', '')).strip()
|
||||||
|
# 店铺状态筛选(1正常,0封禁,不传则全部)
|
||||||
|
zhuangtai = request.data.get('zhuangtai')
|
||||||
|
|
||||||
|
# ---------- 构建查询集(关联凭证表获取用户ID) ----------
|
||||||
|
shops = Dianpu.query.select_related('pingzheng').all()
|
||||||
|
|
||||||
|
# 状态筛选:只接受 0 或 1
|
||||||
|
if zhuangtai is not None:
|
||||||
|
try:
|
||||||
|
zhuangtai = int(zhuangtai)
|
||||||
|
if zhuangtai in [0, 1]:
|
||||||
|
shops = shops.filter(zhuangtai=zhuangtai)
|
||||||
|
else:
|
||||||
|
return Response({'code': 400, 'msg': '状态参数只能为0或1'})
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return Response({'code': 400, 'msg': '状态参数必须为整数'})
|
||||||
|
|
||||||
|
# 关键词搜索(店铺ID/名称/用户ID)
|
||||||
|
if keyword:
|
||||||
|
shops = shops.filter(
|
||||||
|
models.Q(id__icontains=keyword) |
|
||||||
|
models.Q(dianpu_mingcheng__icontains=keyword) |
|
||||||
|
models.Q(pingzheng__yonghu__icontains=keyword)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- 分页 ----------
|
||||||
|
paginator = Paginator(shops, page_size)
|
||||||
|
page_obj = paginator.get_page(page)
|
||||||
|
|
||||||
|
shop_list = []
|
||||||
|
for shop in page_obj:
|
||||||
|
shop_list.append({
|
||||||
|
'id': shop.id,
|
||||||
|
'dianpu_mingcheng': shop.dianpu_mingcheng,
|
||||||
|
'zhuangtai': shop.zhuangtai,
|
||||||
|
'yonghu_id': shop.pingzheng.yonghu if shop.pingzheng else '',
|
||||||
|
'lianxi_dianhua': shop.lianxi_dianhua or '',
|
||||||
|
'weixinhao': shop.weixinhao or '',
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'list': shop_list,
|
||||||
|
'total': paginator.count
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 3. 店铺商品类型映射列表(含筛选、待审核计数) ====================
|
||||||
|
class ShopProductTypeMappingView(APIView):
|
||||||
|
"""
|
||||||
|
返回店铺商品类型映射(ShangpinLeixingDianpu)列表。
|
||||||
|
支持:店铺ID、公共类型ID、上架/封禁/审核状态筛选,关键字搜索(介绍/ID/店铺名/公共类型名)。
|
||||||
|
每次请求同时返回“待审核总数”(shenhe_zhuangtai=False 的记录数)。
|
||||||
|
前端请求路径:/houtai/htdphqyssplx
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username')
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return Response({'code': 403, 'msg': '身份验证失败'})
|
||||||
|
if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
|
||||||
|
return Response({'code': 403, 'msg': '无权访问商品类型映射'})
|
||||||
|
|
||||||
|
# ---------- 安全解析参数 ----------
|
||||||
|
try:
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('page_size', 10))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return Response({'code': 400, 'msg': '分页参数格式错误'})
|
||||||
|
|
||||||
|
keyword = str(request.data.get('keyword', '')).strip()
|
||||||
|
shop_id = request.data.get('shop_id')
|
||||||
|
public_type_id = request.data.get('public_type_id')
|
||||||
|
|
||||||
|
# 布尔型筛选字段处理:None 表示不筛选,否则转为布尔值
|
||||||
|
def parse_bool(val):
|
||||||
|
if val is None:
|
||||||
|
return None
|
||||||
|
return bool(val)
|
||||||
|
|
||||||
|
shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
|
||||||
|
fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
|
||||||
|
shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
|
||||||
|
|
||||||
|
# ---------- 查询(连表店铺和公共类型) ----------
|
||||||
|
mappings = ShangpinLeixingDianpu.query.select_related('dianpu', 'gonggong_leixing').all()
|
||||||
|
|
||||||
|
# 筛选条件:均为参数化,完全防注入
|
||||||
|
if shop_id is not None:
|
||||||
|
mappings = mappings.filter(dianpu_id=int(shop_id))
|
||||||
|
if public_type_id is not None:
|
||||||
|
mappings = mappings.filter(gonggong_leixing_id=int(public_type_id))
|
||||||
|
if shangjia is not None:
|
||||||
|
mappings = mappings.filter(shangjia_zhuangtai=shangjia)
|
||||||
|
if fengjin is not None:
|
||||||
|
mappings = mappings.filter(fengjin_zhuangtai=fengjin)
|
||||||
|
if shenhe is not None:
|
||||||
|
mappings = mappings.filter(shenhe_zhuangtai=shenhe)
|
||||||
|
|
||||||
|
# 关键字搜索(介绍、ID、店铺名、公共类型名)
|
||||||
|
if keyword:
|
||||||
|
mappings = mappings.filter(
|
||||||
|
models.Q(jieshao__icontains=keyword) |
|
||||||
|
models.Q(id__icontains=keyword) |
|
||||||
|
models.Q(dianpu__dianpu_mingcheng__icontains=keyword) |
|
||||||
|
models.Q(gonggong_leixing__jieshao__icontains=keyword)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- 待审核总数(不受分页影响) ----------
|
||||||
|
pending_total = mappings.filter(shenhe_zhuangtai=False).count()
|
||||||
|
|
||||||
|
# ---------- 分页 ----------
|
||||||
|
paginator = Paginator(mappings, page_size)
|
||||||
|
page_obj = paginator.get_page(page)
|
||||||
|
|
||||||
|
mapping_list = []
|
||||||
|
for m in page_obj:
|
||||||
|
mapping_list.append({
|
||||||
|
'id': m.id,
|
||||||
|
'dianpu_id': m.dianpu_id,
|
||||||
|
'dianpu_name': m.dianpu.dianpu_mingcheng if m.dianpu else '',
|
||||||
|
'gonggong_leixing_id': m.gonggong_leixing_id,
|
||||||
|
'gonggong_leixing_name': m.gonggong_leixing.jieshao if m.gonggong_leixing else '',
|
||||||
|
'jieshao': m.jieshao,
|
||||||
|
'tupian_url': m.tupian_url or '',
|
||||||
|
'paixu': m.paixu,
|
||||||
|
'shangjia_zhuangtai': m.shangjia_zhuangtai,
|
||||||
|
'fengjin_zhuangtai': m.fengjin_zhuangtai,
|
||||||
|
'shenhe_zhuangtai': m.shenhe_zhuangtai,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'list': mapping_list,
|
||||||
|
'total': paginator.count,
|
||||||
|
'pending_total': pending_total
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 统一修改接口(已修正事务) ====================
|
||||||
|
class ShopProductModifyView(APIView):
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username')
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return Response({'code': 403, 'msg': '身份验证失败'})
|
||||||
|
|
||||||
|
user_perms = set(permissions)
|
||||||
|
action = request.data.get('action')
|
||||||
|
|
||||||
|
if action == 'update_audit_mode':
|
||||||
|
return self._update_audit_mode(request, user_perms)
|
||||||
|
elif action == 'update_mapping_status':
|
||||||
|
return self._update_mapping_status(request, user_perms)
|
||||||
|
else:
|
||||||
|
return Response({'code': 400, 'msg': '未知操作'})
|
||||||
|
|
||||||
|
@transaction.atomic # ✅ 事务修复点
|
||||||
|
def _update_audit_mode(self, request, user_perms):
|
||||||
|
if '1199ab' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改审核模式'})
|
||||||
|
|
||||||
|
kaiqi = request.data.get('kaiqi_shenhe')
|
||||||
|
if kaiqi is None:
|
||||||
|
return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1)
|
||||||
|
config.kaiqi_shenhe = bool(kaiqi)
|
||||||
|
config.save(update_fields=['kaiqi_shenhe'])
|
||||||
|
return Response({'code': 0, 'msg': '审核模式已更新'})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("更新审核模式失败")
|
||||||
|
return Response({'code': 500, 'msg': '服务器错误'})
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def _update_mapping_status(self, request, user_perms):
|
||||||
|
mapping_id = request.data.get('mapping_id')
|
||||||
|
if not mapping_id:
|
||||||
|
return Response({'code': 400, 'msg': '缺少 mapping_id'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
mapping = ShangpinLeixingDianpu.objects.select_for_update().get(id=int(mapping_id))
|
||||||
|
except (ShangpinLeixingDianpu.DoesNotExist, ValueError):
|
||||||
|
return Response({'code': 404, 'msg': '映射不存在'})
|
||||||
|
|
||||||
|
fengjin = request.data.get('fengjin_zhuangtai')
|
||||||
|
shangjia = request.data.get('shangjia_zhuangtai')
|
||||||
|
shenhe = request.data.get('shenhe_zhuangtai')
|
||||||
|
|
||||||
|
# 权限判断
|
||||||
|
if fengjin is not None:
|
||||||
|
if fengjin:
|
||||||
|
if '1199abc' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限封禁'})
|
||||||
|
else:
|
||||||
|
if '1199abd' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限解封'})
|
||||||
|
mapping.fengjin_zhuangtai = bool(fengjin)
|
||||||
|
|
||||||
|
if shangjia is not None:
|
||||||
|
if not shangjia:
|
||||||
|
if '1199abc' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限下架'})
|
||||||
|
else:
|
||||||
|
if '1199abd' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限上架'})
|
||||||
|
mapping.shangjia_zhuangtai = bool(shangjia)
|
||||||
|
|
||||||
|
if shenhe is not None:
|
||||||
|
if shenhe:
|
||||||
|
if '1199abd' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限审核通过'})
|
||||||
|
mapping.shenhe_zhuangtai = bool(shenhe)
|
||||||
|
|
||||||
|
mapping.save()
|
||||||
|
return Response({'code': 0, 'msg': '修改成功'})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 商品管理相关权限组(拥有任意一个即可查看列表)
|
||||||
|
PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd', '1199abe']
|
||||||
|
|
||||||
|
|
||||||
|
class ProductListView(APIView):
|
||||||
|
"""
|
||||||
|
商品列表接口,支持多条件筛选与分页。
|
||||||
|
请求路径: /houtai/hqhtdpsplx
|
||||||
|
权限要求: 1199ab / 1199abc / 1199abd / 1199abe 任一即可
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# ----- 身份与权限验证 -----
|
||||||
|
username = request.data.get('username')
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return Response({'code': 403, 'msg': '身份验证失败'})
|
||||||
|
|
||||||
|
if not set(permissions).intersection(PRODUCT_PERMS):
|
||||||
|
return Response({'code': 403, 'msg': '无商品管理权限'})
|
||||||
|
|
||||||
|
# ----- 安全解析分页与筛选参数 -----
|
||||||
|
try:
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('page_size', 15))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return Response({'code': 400, 'msg': '分页参数格式错误'})
|
||||||
|
|
||||||
|
keyword = str(request.data.get('keyword', '')).strip()
|
||||||
|
shop_id = request.data.get('shop_id') # 店铺ID (可选)
|
||||||
|
shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型映射ID (可选)
|
||||||
|
public_type_id = request.data.get('public_type_id') # 公共商品类型ID (可选)
|
||||||
|
|
||||||
|
# 布尔型状态筛选
|
||||||
|
def parse_bool(val):
|
||||||
|
if val is None:
|
||||||
|
return None
|
||||||
|
return bool(val)
|
||||||
|
|
||||||
|
shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
|
||||||
|
shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
|
||||||
|
fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
|
||||||
|
|
||||||
|
# ----- 构建查询(预加载店铺和店铺商品类型) -----
|
||||||
|
products = Shangpin.query.select_related('dianpu', 'dianpu_leixing').all()
|
||||||
|
|
||||||
|
# 按店铺筛选
|
||||||
|
if shop_id is not None:
|
||||||
|
try:
|
||||||
|
products = products.filter(dianpu_id=int(shop_id))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return Response({'code': 400, 'msg': 'shop_id 格式错误'})
|
||||||
|
|
||||||
|
# 按店铺商品类型映射筛选
|
||||||
|
if shop_product_type_id is not None:
|
||||||
|
try:
|
||||||
|
products = products.filter(dianpu_leixing_id=int(shop_product_type_id))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'})
|
||||||
|
|
||||||
|
# 按公共商品类型筛选(leixing_id 存储的是公共类型的ID)
|
||||||
|
if public_type_id is not None:
|
||||||
|
try:
|
||||||
|
products = products.filter(leixing_id=int(public_type_id))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return Response({'code': 400, 'msg': 'public_type_id 格式错误'})
|
||||||
|
|
||||||
|
# 平台审核状态筛选 (BooleanField)
|
||||||
|
if shenhe is not None:
|
||||||
|
products = products.filter(shenhe_zhuangtai=shenhe)
|
||||||
|
|
||||||
|
# 上架状态筛选
|
||||||
|
if shangjia is not None:
|
||||||
|
products = products.filter(shangjia_zhuangtai=shangjia)
|
||||||
|
|
||||||
|
# 封禁状态筛选
|
||||||
|
if fengjin is not None:
|
||||||
|
products = products.filter(fengjin_zhuangtai=fengjin)
|
||||||
|
|
||||||
|
# 关键词搜索:商品ID或商品标题(搜索具有模糊性,使用icontains)
|
||||||
|
if keyword:
|
||||||
|
# 尝试纯数字可能是商品ID,使用精确匹配与标题模糊搜索
|
||||||
|
products = products.filter(
|
||||||
|
models.Q(id__icontains=keyword) |
|
||||||
|
models.Q(biaoqian__icontains=keyword)
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----- 获取待审核总数(平台审核未通过的数量) -----
|
||||||
|
pending_total = products.filter(shenhe_zhuangtai=False).count()
|
||||||
|
|
||||||
|
# ----- 分页 -----
|
||||||
|
paginator = Paginator(products, page_size)
|
||||||
|
page_obj = paginator.get_page(page)
|
||||||
|
|
||||||
|
# ----- 构造返回数据(批量获取公共类型名称,避免N+1) -----
|
||||||
|
# 提取所有公共商品类型ID
|
||||||
|
type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None}
|
||||||
|
public_type_map = {}
|
||||||
|
if type_ids:
|
||||||
|
public_types = ShangpinLeixing.query.filter(id__in=type_ids).values('id', 'jieshao')
|
||||||
|
public_type_map = {t['id']: t['jieshao'] for t in public_types}
|
||||||
|
|
||||||
|
product_list = []
|
||||||
|
for p in page_obj:
|
||||||
|
product_list.append({
|
||||||
|
'id': p.id,
|
||||||
|
'biaoqian': p.biaoqian or '',
|
||||||
|
'jiage': p.jiage,
|
||||||
|
'kucun': p.kucun,
|
||||||
|
'leixing_id': p.leixing_id,
|
||||||
|
'public_type_name': public_type_map.get(p.leixing_id, ''),
|
||||||
|
'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
|
||||||
|
'duiwai_xiaoliang': p.duiwai_xiaoliang,
|
||||||
|
'jieshao': p.jieshao or '',
|
||||||
|
'xiadan_xuzhi': p.xiadan_xuzhi or '',
|
||||||
|
'guize_tupian': p.guize_tupian or '',
|
||||||
|
'yaoqiuleixing': p.yaoqiuleixing,
|
||||||
|
'huiyuan_id': p.huiyuan_id or '',
|
||||||
|
'yongjin': p.yongjin,
|
||||||
|
'kaioi_ewai_PlayerCommission': p.kaioi_ewai_dashou_fencheng,
|
||||||
|
'ewai_PlayerCommission': p.ewai_dashou_fencheng,
|
||||||
|
'CreateTime': p.CreateTime.isoformat() if p.CreateTime else '',
|
||||||
|
'UpdateTime': p.UpdateTime.isoformat() if p.UpdateTime else '',
|
||||||
|
'paixu': p.paixu,
|
||||||
|
'dianpu_id': p.dianpu_id,
|
||||||
|
'dianpu_name': p.dianpu.dianpu_mingcheng if p.dianpu else '',
|
||||||
|
'dianpu_leixing_id': p.dianpu_leixing_id,
|
||||||
|
'dianpu_leixing_name': p.dianpu_leixing.jieshao if p.dianpu_leixing else '',
|
||||||
|
'shangjia_zhuangtai': p.shangjia_zhuangtai,
|
||||||
|
'fengjin_zhuangtai': p.fengjin_zhuangtai,
|
||||||
|
'shenhe_zhuangtai': p.shenhe_zhuangtai,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'list': product_list,
|
||||||
|
'total': paginator.count,
|
||||||
|
'pending_total': pending_total, # 待审核商品总数
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class ProductModifyView(APIView):
|
||||||
|
"""
|
||||||
|
商品修改接口,根据修改内容细粒度鉴权。
|
||||||
|
请求路径: /houtai/htxgdpspsj
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username')
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return Response({'code': 403, 'msg': '身份验证失败'})
|
||||||
|
|
||||||
|
user_perms = set(permissions)
|
||||||
|
action = request.data.get('action', '')
|
||||||
|
|
||||||
|
if action != 'update_product':
|
||||||
|
return Response({'code': 400, 'msg': '未知操作'})
|
||||||
|
|
||||||
|
return self._update_product(request, user_perms)
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def _update_product(self, request, user_perms):
|
||||||
|
"""执行商品修改,严格校验权限"""
|
||||||
|
product_id = request.data.get('product_id')
|
||||||
|
if not product_id:
|
||||||
|
return Response({'code': 400, 'msg': '缺少商品ID'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 行级锁防止并发修改
|
||||||
|
product = Shangpin.objects.select_for_update().get(id=int(product_id))
|
||||||
|
except (Shangpin.DoesNotExist, ValueError):
|
||||||
|
return Response({'code': 404, 'msg': '商品不存在'})
|
||||||
|
|
||||||
|
# 需要修改的字段
|
||||||
|
fields_to_update = {}
|
||||||
|
# 将要修改的状态标志用于后续权限判断
|
||||||
|
status_changes = {
|
||||||
|
'shangjia': request.data.get('shangjia_zhuangtai'),
|
||||||
|
'fengjin': request.data.get('fengjin_zhuangtai'),
|
||||||
|
'shenhe': request.data.get('shenhe_zhuangtai'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 权限分类检查 ----------
|
||||||
|
# 1. 封禁/下架操作 (消极操作) -> 1199abc
|
||||||
|
if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']):
|
||||||
|
if '1199abc' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限执行下架/封禁/驳回审核操作(1199abc)'})
|
||||||
|
if status_changes.get('fengjin') is not None and status_changes['fengjin'] == True:
|
||||||
|
if '1199abc' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限执行封禁操作(1199abc)'})
|
||||||
|
|
||||||
|
# 2. 解封/上架/过审 (积极操作) -> 1199abd
|
||||||
|
if any(status_changes.get(k) is not None and status_changes[k] == True for k in ['shangjia', 'shenhe']):
|
||||||
|
if '1199abd' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限执行上架/解封/审核通过操作(1199abd)'})
|
||||||
|
if status_changes.get('fengjin') is not None and status_changes['fengjin'] == False:
|
||||||
|
if '1199abd' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'})
|
||||||
|
|
||||||
|
# 3. 修改其他数据(非状态字段)-> 1199abe
|
||||||
|
# 除了上架、封禁、审核之外的字段都属于“其他数据”
|
||||||
|
other_fields = [k for k in request.data.keys() if k not in ['action', 'product_id', 'username',
|
||||||
|
'shangjia_zhuangtai', 'fengjin_zhuangtai',
|
||||||
|
'shenhe_zhuangtai']]
|
||||||
|
if other_fields and '1199abe' not in user_perms:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'})
|
||||||
|
|
||||||
|
# ---------- 应用修改 ----------
|
||||||
|
# 允许修改的字段白名单(防止注入)
|
||||||
|
# 前端参数名 → 模型字段名映射
|
||||||
|
field_name_map = {
|
||||||
|
'kaioi_ewai_PlayerCommission': 'kaioi_ewai_dashou_fencheng',
|
||||||
|
'ewai_PlayerCommission': 'ewai_dashou_fencheng',
|
||||||
|
}
|
||||||
|
allowed_fields = [
|
||||||
|
'biaoqian', 'jiage', 'kucun', 'leixing_id', 'duiwai_xiaoliang',
|
||||||
|
'jieshao', 'xiadan_xuzhi', 'yaoqiuleixing', 'yongjin',
|
||||||
|
'kaioi_ewai_PlayerCommission', 'ewai_PlayerCommission',
|
||||||
|
'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai'
|
||||||
|
]
|
||||||
|
|
||||||
|
update_fields = []
|
||||||
|
for field in allowed_fields:
|
||||||
|
value = request.data.get(field)
|
||||||
|
if value is not None:
|
||||||
|
model_field = field_name_map.get(field, field)
|
||||||
|
# 简单类型校验(根据字段类型)
|
||||||
|
if field in ['jiage', 'kucun', 'duiwai_xiaoliang', 'yongjin', 'ewai_PlayerCommission', 'paixu']:
|
||||||
|
try:
|
||||||
|
value = float(value) if 'jiage' in field or 'yongjin' in field or 'ewai' in field else int(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return Response({'code': 400, 'msg': f'字段 {field} 值格式错误'})
|
||||||
|
setattr(product, model_field, value)
|
||||||
|
update_fields.append(model_field)
|
||||||
|
|
||||||
|
if update_fields:
|
||||||
|
product.save(update_fields=update_fields)
|
||||||
|
return Response({'code': 0, 'msg': '商品修改成功'})
|
||||||
|
|
||||||
1159
backend/views/system.py
Normal file
1159
backend/views/system.py
Normal file
File diff suppressed because it is too large
Load Diff
347
backend/views/titles.py
Normal file
347
backend/views/titles.py
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
import hmac
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import xmltodict
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from calendar import monthrange
|
||||||
|
from decimal import Decimal
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# 工具类导入
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action,
|
||||||
|
update_shangjia_daily
|
||||||
|
)
|
||||||
|
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||||
|
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||||
|
from jituan.services.club_penalty import (
|
||||||
|
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||||
|
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||||
|
)
|
||||||
|
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
from ..utils import (
|
||||||
|
verify_kefu_permission,
|
||||||
|
PERMISSION_TO_SHENFEN,
|
||||||
|
SHENFEN_PROFILE_MAP,
|
||||||
|
has_fadan_view_permission,
|
||||||
|
has_merchant_order_permission,
|
||||||
|
check_fadan_permission,
|
||||||
|
pick_order_id,
|
||||||
|
pick_penalty_create_params,
|
||||||
|
write_xiugai_log,
|
||||||
|
XIUGAI_LEIXING_DASHOU,
|
||||||
|
XIUGAI_LEIXING_GUANSHI,
|
||||||
|
XIUGAI_LEIXING_SHANGJIA,
|
||||||
|
XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
XIUGAI_LEIXING_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# models 集中导入
|
||||||
|
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||||
|
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||||
|
## backend
|
||||||
|
from backend.models import WithdrawalDailyStats
|
||||||
|
|
||||||
|
## users
|
||||||
|
from users.models import (
|
||||||
|
UserGuanshi, UserBoss, UserZuzhang,
|
||||||
|
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
## orders
|
||||||
|
from orders.models import (
|
||||||
|
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||||
|
OrderPlayerHistory, Order, RefundRecord,
|
||||||
|
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||||
|
)
|
||||||
|
|
||||||
|
## shop
|
||||||
|
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||||
|
|
||||||
|
## products
|
||||||
|
from products.models import (
|
||||||
|
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||||
|
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||||
|
)
|
||||||
|
|
||||||
|
## config
|
||||||
|
from config.models import (
|
||||||
|
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||||
|
TixianQuotaDefault,
|
||||||
|
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||||
|
)
|
||||||
|
|
||||||
|
## rank
|
||||||
|
from rank.models import (
|
||||||
|
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||||
|
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||||
|
)
|
||||||
|
|
||||||
|
# 序列化器
|
||||||
|
from ..serializers import PopupPageSerializer
|
||||||
|
|
||||||
|
# 全局常量、日志对象
|
||||||
|
logger = logging.getLogger('houtai')
|
||||||
|
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||||
|
|
||||||
|
class KhpzhqView(APIView):
|
||||||
|
"""
|
||||||
|
获取所有考核配置数据(称号列表、板块、费用)
|
||||||
|
POST /houtai/khpzhq
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
if 'kaohepeizhi' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无考核配置权限'}, status=403)
|
||||||
|
|
||||||
|
bankuai_list = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
|
||||||
|
|
||||||
|
chenghao_qs = Chenghao.query.prefetch_related('kaohecishufeiyong_set').all()
|
||||||
|
chenghao_list = []
|
||||||
|
for ch in chenghao_qs:
|
||||||
|
# 获取费用:按次数排序,转为 {cishu: feiyong} 字典
|
||||||
|
feiyong_dict = {
|
||||||
|
item.cishu: float(item.feiyong)
|
||||||
|
for item in ch.kaohecishufeiyong_set.order_by('cishu')
|
||||||
|
}
|
||||||
|
max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0
|
||||||
|
|
||||||
|
chenghao_list.append({
|
||||||
|
'id': ch.id,
|
||||||
|
'mingcheng': ch.mingcheng,
|
||||||
|
'leixing': ch.leixing,
|
||||||
|
'leixing_name': ch.get_leixing_display(),
|
||||||
|
'bankuai_id': ch.bankuai_id,
|
||||||
|
'tubiao_url': ch.tubiao_url or '',
|
||||||
|
'texiao_miaoshu': ch.texiao_miaoshu or '',
|
||||||
|
'CreateTime': ch.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if ch.CreateTime else '',
|
||||||
|
'kaohe_guize': ch.kaohe_guize or '',
|
||||||
|
'kaioi_jinpai': ch.kaioi_jinpai,
|
||||||
|
'feiyong': feiyong_dict, # {1:100.0, 2:200.0}
|
||||||
|
'max_cishu': max_cishu
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'ok',
|
||||||
|
'data': {
|
||||||
|
'bankuai': bankuai_list,
|
||||||
|
'chenghao': chenghao_list
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("KhpzhqView 接口错误")
|
||||||
|
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ChzsgcView(APIView):
|
||||||
|
"""
|
||||||
|
称号(标签)增删改查统一接口
|
||||||
|
POST /houtai/chzsgc
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
username_from_frontend = request.data.get('phone', None)
|
||||||
|
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||||
|
if kefu_obj is None:
|
||||||
|
return permissions
|
||||||
|
if 'kaohepeizhi' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无考核配置权限'}, status=403)
|
||||||
|
|
||||||
|
action = request.data.get('action')
|
||||||
|
if not action:
|
||||||
|
return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
|
||||||
|
|
||||||
|
# ========== 创建称号 ==========
|
||||||
|
if action == 'create':
|
||||||
|
mingcheng = request.data.get('mingcheng', '').strip()
|
||||||
|
leixing = request.data.get('leixing', 'dashou')
|
||||||
|
bankuai_id = request.data.get('bankuai_id')
|
||||||
|
texiao_miaoshu = request.data.get('texiao_miaoshu', '')
|
||||||
|
kaohe_guize = request.data.get('kaohe_guize', '')
|
||||||
|
kaioi_jinpai = request.data.get('kaioi_jinpai', False)
|
||||||
|
feiyong_list = request.data.get('feiyong_list', [])
|
||||||
|
|
||||||
|
if not mingcheng:
|
||||||
|
return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
|
||||||
|
if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
|
||||||
|
return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
|
||||||
|
if Chenghao.query.filter(mingcheng=mingcheng).exists():
|
||||||
|
return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
ch = Chenghao.query.create(
|
||||||
|
mingcheng=mingcheng,
|
||||||
|
leixing=leixing,
|
||||||
|
bankuai_id=bankuai_id,
|
||||||
|
texiao_miaoshu=texiao_miaoshu,
|
||||||
|
kaohe_guize=kaohe_guize,
|
||||||
|
kaioi_jinpai=kaioi_jinpai
|
||||||
|
)
|
||||||
|
# 处理费用:前端传来 [{cishu:1, feiyong:100}, ...]
|
||||||
|
if feiyong_list:
|
||||||
|
for item in feiyong_list:
|
||||||
|
cishu = int(item.get('cishu', 1))
|
||||||
|
feiyong = float(item.get('feiyong', 0))
|
||||||
|
KaoheCishuFeiyong.query.update_or_create(
|
||||||
|
chenghao=ch, cishu=cishu,
|
||||||
|
defaults={'feiyong': feiyong}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 默认添加第一次免费
|
||||||
|
KaoheCishuFeiyong.query.create(chenghao=ch, cishu=1, feiyong=0)
|
||||||
|
|
||||||
|
return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}})
|
||||||
|
|
||||||
|
# ========== 修改称号 ==========
|
||||||
|
elif action == 'update':
|
||||||
|
ch_id = request.data.get('chenghao_id')
|
||||||
|
if not ch_id:
|
||||||
|
return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
|
||||||
|
try:
|
||||||
|
ch = Chenghao.query.get(id=ch_id)
|
||||||
|
except Chenghao.DoesNotExist:
|
||||||
|
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
|
||||||
|
|
||||||
|
mingcheng = request.data.get('mingcheng', ch.mingcheng).strip()
|
||||||
|
leixing = request.data.get('leixing', ch.leixing)
|
||||||
|
bankuai_id = request.data.get('bankuai_id', ch.bankuai_id)
|
||||||
|
texiao_miaoshu = request.data.get('texiao_miaoshu', ch.texiao_miaoshu)
|
||||||
|
kaohe_guize = request.data.get('kaohe_guize', ch.kaohe_guize)
|
||||||
|
kaioi_jinpai = request.data.get('kaioi_jinpai', ch.kaioi_jinpai)
|
||||||
|
|
||||||
|
if not mingcheng:
|
||||||
|
return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
|
||||||
|
if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
|
||||||
|
return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
|
||||||
|
if Chenghao.query.filter(mingcheng=mingcheng).exclude(id=ch_id).exists():
|
||||||
|
return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
|
||||||
|
|
||||||
|
ch.mingcheng = mingcheng
|
||||||
|
ch.leixing = leixing
|
||||||
|
ch.bankuai_id = bankuai_id
|
||||||
|
ch.texiao_miaoshu = texiao_miaoshu
|
||||||
|
ch.kaohe_guize = kaohe_guize
|
||||||
|
ch.kaioi_jinpai = kaioi_jinpai
|
||||||
|
ch.save()
|
||||||
|
|
||||||
|
# 如果前端传递了 feiyong_list,则全量替换费用
|
||||||
|
feiyong_list = request.data.get('feiyong_list')
|
||||||
|
if feiyong_list is not None:
|
||||||
|
with transaction.atomic():
|
||||||
|
ch.kaohecishufeiyong_set.all().delete()
|
||||||
|
for item in feiyong_list:
|
||||||
|
KaoheCishuFeiyong.query.create(
|
||||||
|
chenghao=ch,
|
||||||
|
cishu=int(item.get('cishu', 1)),
|
||||||
|
feiyong=float(item.get('feiyong', 0))
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response({'code': 0, 'msg': '修改成功'})
|
||||||
|
|
||||||
|
# ========== 删除称号 ==========
|
||||||
|
elif action == 'delete':
|
||||||
|
ch_id = request.data.get('chenghao_id')
|
||||||
|
if not ch_id:
|
||||||
|
return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
|
||||||
|
try:
|
||||||
|
ch = Chenghao.query.get(id=ch_id)
|
||||||
|
except Chenghao.DoesNotExist:
|
||||||
|
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
|
||||||
|
ch.delete()
|
||||||
|
return Response({'code': 0, 'msg': '删除成功'})
|
||||||
|
|
||||||
|
# ========== 添加/修改某次费用 ==========
|
||||||
|
elif action == 'add_feiyong':
|
||||||
|
ch_id = request.data.get('chenghao_id')
|
||||||
|
cishu = request.data.get('cishu')
|
||||||
|
feiyong = request.data.get('feiyong', 0)
|
||||||
|
if not ch_id or not cishu:
|
||||||
|
return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
|
||||||
|
try:
|
||||||
|
ch = Chenghao.query.get(id=ch_id)
|
||||||
|
except Chenghao.DoesNotExist:
|
||||||
|
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
|
||||||
|
if int(cishu) < 1:
|
||||||
|
return Response({'code': 1, 'msg': '次数必须>=1'}, status=400)
|
||||||
|
KaoheCishuFeiyong.query.update_or_create(
|
||||||
|
chenghao=ch, cishu=cishu,
|
||||||
|
defaults={'feiyong': feiyong}
|
||||||
|
)
|
||||||
|
return Response({'code': 0, 'msg': '添加/修改费用成功'})
|
||||||
|
|
||||||
|
# ========== 删除某次费用 ==========
|
||||||
|
elif action == 'remove_feiyong':
|
||||||
|
ch_id = request.data.get('chenghao_id')
|
||||||
|
cishu = request.data.get('cishu')
|
||||||
|
if not ch_id or not cishu:
|
||||||
|
return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
|
||||||
|
if int(cishu) == 1:
|
||||||
|
return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400)
|
||||||
|
try:
|
||||||
|
ch = Chenghao.query.get(id=ch_id)
|
||||||
|
except Chenghao.DoesNotExist:
|
||||||
|
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
|
||||||
|
KaoheCishuFeiyong.query.filter(chenghao=ch, cishu=cishu).delete()
|
||||||
|
return Response({'code': 0, 'msg': '删除成功'})
|
||||||
|
|
||||||
|
# ========== 修改某次费用(等价于add) ==========
|
||||||
|
elif action == 'update_feiyong':
|
||||||
|
return self.add_feiyong(request) # 复用逻辑
|
||||||
|
|
||||||
|
else:
|
||||||
|
return Response({'code': 1, 'msg': '未知操作'}, status=400)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("ChzsgcView 接口错误")
|
||||||
|
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 后端 views.py ====================
|
||||||
|
# 请确保已有以下导入,如果没有请在文件头部添加
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
170
backend/views/withdraw_settings.py
Normal file
170
backend/views/withdraw_settings.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import hmac
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import xmltodict
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from calendar import monthrange
|
||||||
|
from decimal import Decimal
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# 工具类导入
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action,
|
||||||
|
update_shangjia_daily
|
||||||
|
)
|
||||||
|
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||||
|
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||||
|
from jituan.services.club_penalty import (
|
||||||
|
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||||
|
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||||
|
)
|
||||||
|
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
from ..utils import (
|
||||||
|
verify_kefu_permission,
|
||||||
|
PERMISSION_TO_SHENFEN,
|
||||||
|
SHENFEN_PROFILE_MAP,
|
||||||
|
has_fadan_view_permission,
|
||||||
|
has_merchant_order_permission,
|
||||||
|
check_fadan_permission,
|
||||||
|
pick_order_id,
|
||||||
|
pick_penalty_create_params,
|
||||||
|
write_xiugai_log,
|
||||||
|
XIUGAI_LEIXING_DASHOU,
|
||||||
|
XIUGAI_LEIXING_GUANSHI,
|
||||||
|
XIUGAI_LEIXING_SHANGJIA,
|
||||||
|
XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
XIUGAI_LEIXING_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# models 集中导入
|
||||||
|
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||||
|
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||||
|
## backend
|
||||||
|
from backend.models import WithdrawalDailyStats
|
||||||
|
|
||||||
|
## users
|
||||||
|
from users.models import (
|
||||||
|
UserGuanshi, UserBoss, UserZuzhang,
|
||||||
|
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
## orders
|
||||||
|
from orders.models import (
|
||||||
|
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||||
|
OrderPlayerHistory, Order, RefundRecord,
|
||||||
|
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||||
|
)
|
||||||
|
|
||||||
|
## shop
|
||||||
|
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||||
|
|
||||||
|
## products
|
||||||
|
from products.models import (
|
||||||
|
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||||
|
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||||
|
)
|
||||||
|
|
||||||
|
## config
|
||||||
|
from config.models import (
|
||||||
|
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||||
|
TixianQuotaDefault,
|
||||||
|
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||||
|
)
|
||||||
|
|
||||||
|
## rank
|
||||||
|
from rank.models import (
|
||||||
|
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||||
|
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||||
|
)
|
||||||
|
|
||||||
|
# 序列化器
|
||||||
|
from ..serializers import PopupPageSerializer
|
||||||
|
|
||||||
|
# 全局常量、日志对象
|
||||||
|
logger = logging.getLogger('houtai')
|
||||||
|
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||||
|
|
||||||
|
class GetWithdrawSettingsView(APIView):
|
||||||
|
"""
|
||||||
|
获取提现设置(手续费利率、每日限额、当日已提现总额)
|
||||||
|
权限:需要 5500a 或 5500b 或 5500c 任一
|
||||||
|
POST /houtai/hthqtxpz
|
||||||
|
"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
# 需要5500a/b/c任一
|
||||||
|
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
|
||||||
|
return Response({'code': 403, 'msg': '无权限查看提现设置'})
|
||||||
|
|
||||||
|
from jituan.services.withdraw_config import build_withdraw_settings_payload
|
||||||
|
return Response({'code': 0, 'data': build_withdraw_settings_payload(request)})
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateWithdrawSettingsView(APIView):
|
||||||
|
"""
|
||||||
|
修改提现设置(权限细分):
|
||||||
|
- 修改打手相关(利率、个人限额、总限额)需要5500a
|
||||||
|
- 修改管事相关需要5500b
|
||||||
|
- 修改组长相关需要5500c
|
||||||
|
注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算),
|
||||||
|
后端不直接修改 WithdrawalDailyStats,该表由提现流程自动累加。
|
||||||
|
POST /houtai/htxgtxsz
|
||||||
|
"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
# 权限映射:要修改的角色需要对应的权限
|
||||||
|
# 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3
|
||||||
|
from jituan.services.withdraw_config import apply_withdraw_settings_update
|
||||||
|
return apply_withdraw_settings_update(request, permissions)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
791
backend/views/zuzhang.py
Normal file
791
backend/views/zuzhang.py
Normal file
@@ -0,0 +1,791 @@
|
|||||||
|
import hmac
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import xmltodict
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from calendar import monthrange
|
||||||
|
from decimal import Decimal
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# 工具类导入
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action,
|
||||||
|
update_shangjia_daily
|
||||||
|
)
|
||||||
|
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||||
|
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||||
|
from jituan.services.club_penalty import (
|
||||||
|
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||||
|
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||||
|
)
|
||||||
|
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
from ..utils import (
|
||||||
|
verify_kefu_permission,
|
||||||
|
PERMISSION_TO_SHENFEN,
|
||||||
|
SHENFEN_PROFILE_MAP,
|
||||||
|
has_fadan_view_permission,
|
||||||
|
has_merchant_order_permission,
|
||||||
|
check_fadan_permission,
|
||||||
|
pick_order_id,
|
||||||
|
pick_penalty_create_params,
|
||||||
|
write_xiugai_log,
|
||||||
|
XIUGAI_LEIXING_DASHOU,
|
||||||
|
XIUGAI_LEIXING_GUANSHI,
|
||||||
|
XIUGAI_LEIXING_SHANGJIA,
|
||||||
|
XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
XIUGAI_LEIXING_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# models 集中导入
|
||||||
|
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||||
|
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||||
|
## backend
|
||||||
|
from backend.models import WithdrawalDailyStats
|
||||||
|
|
||||||
|
## users
|
||||||
|
from users.models import (
|
||||||
|
UserGuanshi, UserBoss, UserZuzhang,
|
||||||
|
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
## orders
|
||||||
|
from orders.models import (
|
||||||
|
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||||
|
OrderPlayerHistory, Order, RefundRecord,
|
||||||
|
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||||
|
)
|
||||||
|
|
||||||
|
## shop
|
||||||
|
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||||
|
|
||||||
|
## products
|
||||||
|
from products.models import (
|
||||||
|
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||||
|
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||||
|
)
|
||||||
|
|
||||||
|
## config
|
||||||
|
from config.models import (
|
||||||
|
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||||
|
TixianQuotaDefault,
|
||||||
|
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||||
|
)
|
||||||
|
|
||||||
|
## rank
|
||||||
|
from rank.models import (
|
||||||
|
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||||
|
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||||
|
)
|
||||||
|
|
||||||
|
# 序列化器
|
||||||
|
from ..serializers import PopupPageSerializer
|
||||||
|
|
||||||
|
# 全局常量、日志对象
|
||||||
|
logger = logging.getLogger('houtai')
|
||||||
|
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||||
|
|
||||||
|
class GetZuzhangListView(APIView):
|
||||||
|
"""
|
||||||
|
组长列表接口(分页 + 多条件筛选)
|
||||||
|
路由:POST /houtai/hthqzzlb
|
||||||
|
权限:需要登录,且拥有 6600a~6600e 任一权限
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
@transaction.atomic # 保证查询的一致性(可选,对只读查询影响不大但无坏处)
|
||||||
|
def post(self, request):
|
||||||
|
# 1. 获取前端传递的 username(用于防越权)
|
||||||
|
frontend_username = request.data.get('username', '')
|
||||||
|
|
||||||
|
# 2. 调用公共校验方法,验证身份并获取权限列表
|
||||||
|
result = verify_kefu_permission(request, username_from_frontend=frontend_username)
|
||||||
|
|
||||||
|
# 重要:verify_kefu_permission 返回格式为 (user_obj, permissions_list) 或 (None, Response)
|
||||||
|
# 如果第二个元素是 Response 对象,说明校验失败,直接返回该 Response
|
||||||
|
if isinstance(result, tuple) and len(result) == 2:
|
||||||
|
user_obj, second = result
|
||||||
|
if isinstance(second, Response):
|
||||||
|
return second # 校验失败,直接返回错误响应
|
||||||
|
else:
|
||||||
|
permissions = second # 正常情况:permissions 是列表
|
||||||
|
else:
|
||||||
|
# 防止意外返回格式,视为失败
|
||||||
|
return Response({'code': 500, 'msg': '权限校验模块异常'})
|
||||||
|
|
||||||
|
# 3. 检查是否拥有组长管理所需权限(6600a ~ 6600e 任一)
|
||||||
|
required_perms = ['6600a', '6600b', '6600c', '6600d', '6600e']
|
||||||
|
if not any(perm in permissions for perm in required_perms):
|
||||||
|
logger.warning(f"用户 {request.user.Phone} 尝试访问组长列表但缺少必要权限,已有权限: {permissions}")
|
||||||
|
return Response({'code': 403, 'msg': '您无权访问组长管理页面'})
|
||||||
|
|
||||||
|
# 4. 提取请求参数(分页 + 筛选)
|
||||||
|
try:
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('page_size', 20))
|
||||||
|
except ValueError:
|
||||||
|
page, page_size = 1, 20
|
||||||
|
|
||||||
|
keyword = request.data.get('keyword', '').strip()
|
||||||
|
status = request.data.get('status') # 1=正常, 0=禁用
|
||||||
|
invite_count_op = request.data.get('invite_count_op') # gte / lte
|
||||||
|
invite_count_value = request.data.get('invite_count_value')
|
||||||
|
amount_op = request.data.get('amount_op')
|
||||||
|
amount_value = request.data.get('amount_value')
|
||||||
|
commission_op = request.data.get('commission_op')
|
||||||
|
commission_value = request.data.get('commission_value')
|
||||||
|
|
||||||
|
# 5. 构建基础 QuerySet(预加载关联表)
|
||||||
|
queryset = filter_user_related_by_club(
|
||||||
|
UserZuzhang.query.select_related('user', 'user__BossProfile').all(),
|
||||||
|
request,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6. 应用筛选条件(使用 ORM 安全过滤)
|
||||||
|
if keyword:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
Q(user__UserUID__icontains=keyword) |
|
||||||
|
Q(user__BossProfile__nickname__icontains=keyword)
|
||||||
|
)
|
||||||
|
|
||||||
|
if status is not None and status != '':
|
||||||
|
try:
|
||||||
|
queryset = queryset.filter(zhuangtai=int(status))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 邀请总数筛选
|
||||||
|
if invite_count_value not in (None, ''):
|
||||||
|
try:
|
||||||
|
val = float(invite_count_value)
|
||||||
|
op = invite_count_op if invite_count_op in ('gte', 'lte') else 'gte'
|
||||||
|
queryset = queryset.filter(**{f'yaoqing_zongshu__{op}': val})
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 可提现金额筛选
|
||||||
|
if amount_value not in (None, ''):
|
||||||
|
try:
|
||||||
|
val = float(amount_value)
|
||||||
|
op = amount_op if amount_op in ('gte', 'lte') else 'gte'
|
||||||
|
queryset = queryset.filter(**{f'ketixian_jine__{op}': val})
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 分佣总额筛选
|
||||||
|
if commission_value not in (None, ''):
|
||||||
|
try:
|
||||||
|
val = float(commission_value)
|
||||||
|
op = commission_op if commission_op in ('gte', 'lte') else 'gte'
|
||||||
|
queryset = queryset.filter(**{f'fenyong_zonge__{op}': val})
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 7. 总数统计
|
||||||
|
total = queryset.count()
|
||||||
|
|
||||||
|
# 8. 分页
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
paged_queryset = queryset[offset:offset + page_size]
|
||||||
|
|
||||||
|
# 9. 构造返回数据
|
||||||
|
data_list = []
|
||||||
|
for zu in paged_queryset:
|
||||||
|
user = zu.user
|
||||||
|
nickname = ''
|
||||||
|
if hasattr(user, 'BossProfile') and user.BossProfile:
|
||||||
|
nickname = user.BossProfile.nickname or ''
|
||||||
|
|
||||||
|
data_list.append({
|
||||||
|
'yonghuid': user.UserUID,
|
||||||
|
'avatar': user.Avatar or '',
|
||||||
|
'nickname': nickname,
|
||||||
|
'fenyong_zonge': float(zu.fenyong_zonge),
|
||||||
|
'ketixian_jine': float(zu.ketixian_jine),
|
||||||
|
'yaoqing_zongshu': zu.yaoqing_zongshu,
|
||||||
|
'zhuangtai': zu.zhuangtai,
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'success',
|
||||||
|
**list_response_meta(request),
|
||||||
|
'data': {
|
||||||
|
'list': data_list,
|
||||||
|
'total': total,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class GetZuzhangDetailView(APIView):
|
||||||
|
"""
|
||||||
|
获取组长详情
|
||||||
|
权限:6600a,6600b,6600c,6600d,6600e 任一
|
||||||
|
POST /houtai/hthqzzxq
|
||||||
|
参数:username, yonghuid
|
||||||
|
返回:
|
||||||
|
code:0, data: {
|
||||||
|
base: {...},
|
||||||
|
balance: {...},
|
||||||
|
permanent_enabled, permanent_amount,
|
||||||
|
custom_first_dividend: {},
|
||||||
|
extra_dividend_map: {},
|
||||||
|
all_members: []
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
yonghuid = request.data.get('yonghuid', '').strip()
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
if not yonghuid:
|
||||||
|
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||||||
|
|
||||||
|
# 权限校验(需要任一权限)
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
required_perms = {'6600a', '6600b', '6600c', '6600d', '6600e'}
|
||||||
|
if not any(p in permissions for p in required_perms):
|
||||||
|
return Response({'code': 403, 'msg': '您没有权限查看组长详情'})
|
||||||
|
|
||||||
|
# 查询组长主表、扩展表、老板扩展表(昵称)
|
||||||
|
try:
|
||||||
|
user = User.query.select_related('ZuzhangProfile', 'BossProfile').get(UserUID=yonghuid)
|
||||||
|
zuzhang = user.ZuzhangProfile
|
||||||
|
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '组长不存在'})
|
||||||
|
except UserZuzhang.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '用户不是组长'})
|
||||||
|
|
||||||
|
denied = forbid_if_user_out_of_scope(request, user)
|
||||||
|
if denied:
|
||||||
|
return denied
|
||||||
|
|
||||||
|
# 基础信息
|
||||||
|
base_data = {
|
||||||
|
'yonghuid': user.UserUID,
|
||||||
|
'nickname': boss.nickname if boss else '',
|
||||||
|
'avatar': user.Avatar or '',
|
||||||
|
'dianhua': '', # 组长表无电话字段?组长扩展表没有电话/微信,但前端可能需要,可从User关联?实际上组长表无电话字段,这里留空或从其他表获取
|
||||||
|
'wechat': '',
|
||||||
|
'zhuangtai': zuzhang.zhuangtai,
|
||||||
|
'yaoqingma': zuzhang.yaoqingma,
|
||||||
|
'yaoqingren': '', # 组长表没有邀请人字段
|
||||||
|
'haibao_url': zuzhang.haibao_url or '',
|
||||||
|
'yaoqing_zongshu': zuzhang.yaoqing_zongshu,
|
||||||
|
'jinri_fenyong': str(zuzhang.jinri_fenyong),
|
||||||
|
'jinyue_fenyong': str(zuzhang.jinyue_fenyong),
|
||||||
|
'fenyong_zonge': str(zuzhang.fenyong_zonge),
|
||||||
|
'CreateTime': zuzhang.CreateTime.isoformat() if zuzhang.CreateTime else None,
|
||||||
|
'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
|
||||||
|
}
|
||||||
|
|
||||||
|
# 余额提现相关
|
||||||
|
balance_data = {
|
||||||
|
'ketixian_jine': str(zuzhang.ketixian_jine),
|
||||||
|
'jinri_tixian': str(zuzhang.jinri_tixian),
|
||||||
|
'last_tixian_time': zuzhang.last_tixian_time.isoformat() if zuzhang.last_tixian_time else None,
|
||||||
|
'kaioi_ewai_tixian': zuzhang.kaioi_ewai_tixian,
|
||||||
|
'ewai_tixian_xiane': str(zuzhang.ewai_tixian_xiane),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 永久分红(额外分红开关和金额)
|
||||||
|
permanent_enabled = zuzhang.kaioi_ewai_fenhong
|
||||||
|
permanent_amount = str(zuzhang.ewai_fenhong_jine) if zuzhang.ewai_fenhong_jine else '0'
|
||||||
|
|
||||||
|
# 获取所有会员(用于前端选择)
|
||||||
|
all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc'))
|
||||||
|
for m in all_members:
|
||||||
|
m['jiage'] = str(m['jiage'])
|
||||||
|
m['zuzhangfc'] = str(m['zuzhangfc'])
|
||||||
|
|
||||||
|
# 获取多次分红配置(按次数和会员组织,按俱乐部隔离)
|
||||||
|
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||||
|
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||||
|
duoci_records = DuociFenhong.query.filter(
|
||||||
|
yonghuid=yonghuid, club_id=zuzhang_club,
|
||||||
|
).order_by('cishu')
|
||||||
|
custom_first = {} # 首次分红定制(cishu=1)
|
||||||
|
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } }
|
||||||
|
|
||||||
|
for record in duoci_records:
|
||||||
|
# 获取会员信息
|
||||||
|
member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first()
|
||||||
|
if not member:
|
||||||
|
continue
|
||||||
|
member_info = {
|
||||||
|
'jieshao': member.jieshao,
|
||||||
|
'jiage': str(member.jiage),
|
||||||
|
}
|
||||||
|
if record.cishu == 1:
|
||||||
|
custom_first[record.huiyuan] = float(record.zuzhang_fenhong)
|
||||||
|
else:
|
||||||
|
times = record.cishu
|
||||||
|
if times not in extra_map:
|
||||||
|
extra_map[times] = {}
|
||||||
|
extra_map[times][record.huiyuan] = {
|
||||||
|
'zuzhang_fenhong': float(record.zuzhang_fenhong),
|
||||||
|
**member_info
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'base': base_data,
|
||||||
|
'balance': balance_data,
|
||||||
|
'permanent_enabled': permanent_enabled,
|
||||||
|
'permanent_amount': permanent_amount,
|
||||||
|
'custom_first_dividend': custom_first,
|
||||||
|
'extra_dividend_map': extra_map,
|
||||||
|
'all_members': all_members,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateZuzhangView(APIView):
|
||||||
|
"""
|
||||||
|
修改组长信息(基础信息、余额、提现限额、分红策略)
|
||||||
|
权限细分:
|
||||||
|
- 增加可提现金额:6600a
|
||||||
|
- 减少可提现金额:6600b
|
||||||
|
- 修改状态(封禁/解封):6600c
|
||||||
|
- 修改提现限额(kaioi_ewai_tixian/ewai_tixian_xiane):6600d
|
||||||
|
- 修改分红相关(永久分红、定制首次、额外次数):6600e
|
||||||
|
POST /houtai/htxgzzxx
|
||||||
|
参数:
|
||||||
|
username, yonghuid,
|
||||||
|
nickname, avatar, dianhua, wechat, zhuangtai, # 基础
|
||||||
|
ketixian_jine, kaioi_ewai_tixian, ewai_tixian_xiane, # 余额提现
|
||||||
|
permanent_enabled, permanent_amount, # 永久分红
|
||||||
|
custom_first_dividend: {}, # 首次定制分红 {会员ID: 金额}
|
||||||
|
extra_dividend_map: {} # 额外次数分红 {次数: {会员ID: {zuzhang_fenhong}}}
|
||||||
|
"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
yonghuid = request.data.get('yonghuid', '').strip()
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
if not yonghuid:
|
||||||
|
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||||||
|
|
||||||
|
# 权限校验
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
# 查询组长对象
|
||||||
|
try:
|
||||||
|
user = User.query.get(UserUID=yonghuid)
|
||||||
|
zuzhang = user.ZuzhangProfile
|
||||||
|
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '用户不存在'})
|
||||||
|
except UserZuzhang.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '用户不是组长'})
|
||||||
|
|
||||||
|
# 获取原始数据(用于对比金额变化)
|
||||||
|
old_ketixian_jine = zuzhang.ketixian_jine
|
||||||
|
|
||||||
|
# 开始事务
|
||||||
|
with transaction.atomic():
|
||||||
|
operator = kefu.user.Phone
|
||||||
|
old_nickname = boss.nickname if boss else ''
|
||||||
|
old_zhuangtai = zuzhang.zhuangtai
|
||||||
|
# ---------- 1. 基础信息修改(需要6600c?按需求,基础信息修改通常用6600c,但为了安全,暂不校验,如有需要可加)----------
|
||||||
|
nickname = request.data.get('nickname', '').strip()
|
||||||
|
avatar = request.data.get('avatar', '').strip()
|
||||||
|
dianhua = request.data.get('dianhua', '').strip()
|
||||||
|
wechat = request.data.get('wechat', '').strip()
|
||||||
|
zhuangtai = request.data.get('zhuangtai')
|
||||||
|
base_changes = []
|
||||||
|
# 状态修改需要6600c
|
||||||
|
if zhuangtai is not None:
|
||||||
|
if '6600c' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改组长状态(需要6600c)'})
|
||||||
|
try:
|
||||||
|
zt = int(zhuangtai)
|
||||||
|
if zt in (0, 1):
|
||||||
|
zuzhang.zhuangtai = zt
|
||||||
|
if zt != old_zhuangtai:
|
||||||
|
zt_map = {0: '禁用', 1: '正常'}
|
||||||
|
base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}→{zt_map.get(zt, zt)}')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if nickname:
|
||||||
|
if not boss:
|
||||||
|
boss = UserBoss.query.create(user=user, nickname=nickname)
|
||||||
|
else:
|
||||||
|
boss.nickname = nickname
|
||||||
|
boss.save()
|
||||||
|
if nickname != old_nickname:
|
||||||
|
base_changes.append(f'昵称:{old_nickname or "无"}→{nickname}')
|
||||||
|
if avatar:
|
||||||
|
user.Avatar = avatar
|
||||||
|
user.save()
|
||||||
|
base_changes.append('更新了头像')
|
||||||
|
if base_changes:
|
||||||
|
zuzhang.save()
|
||||||
|
write_xiugai_log(
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
xiugaiid=operator,
|
||||||
|
leixing=XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
qitashuoming=(
|
||||||
|
f'【后台】修改组长基础信息:{";".join(base_changes)},'
|
||||||
|
f'组长ID={yonghuid},操作人={operator}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# 组长表没有电话/微信字段,如需存储可暂存到User或忽略
|
||||||
|
# 这里不处理
|
||||||
|
|
||||||
|
# ---------- 2. 可提现金额修改(加:6600a;减:6600b)----------
|
||||||
|
new_ketixian_jine = request.data.get('ketixian_jine')
|
||||||
|
if new_ketixian_jine is not None:
|
||||||
|
try:
|
||||||
|
new_val = Decimal(str(new_ketixian_jine))
|
||||||
|
if new_val < 0:
|
||||||
|
return Response({'code': 400, 'msg': '可提现金额不能为负数'})
|
||||||
|
except:
|
||||||
|
return Response({'code': 400, 'msg': '金额格式错误'})
|
||||||
|
if new_val > old_ketixian_jine:
|
||||||
|
if '6600a' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限增加可提现金额(需要6600a)'})
|
||||||
|
from jituan.services.group_finance_gate import deny_group_finance_exec
|
||||||
|
deny = deny_group_finance_exec(request.user, request)
|
||||||
|
if deny:
|
||||||
|
return deny
|
||||||
|
action_desc = f'增加{new_val - old_ketixian_jine}元'
|
||||||
|
elif new_val < old_ketixian_jine:
|
||||||
|
if '6600b' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限减少可提现金额(需要6600b)'})
|
||||||
|
action_desc = f'减少{old_ketixian_jine - new_val}元'
|
||||||
|
else:
|
||||||
|
action_desc = '无变动'
|
||||||
|
if new_val != old_ketixian_jine:
|
||||||
|
zuzhang.ketixian_jine = new_val
|
||||||
|
zuzhang.save(update_fields=['ketixian_jine'])
|
||||||
|
write_xiugai_log(
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
xiugaiid=operator,
|
||||||
|
leixing=XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
xiugaitijiao=new_val,
|
||||||
|
xiugaitijiaoq=old_ketixian_jine,
|
||||||
|
qitashuoming=(
|
||||||
|
f'【后台】修改组长可提现金额:{old_ketixian_jine}元 → {new_val}元({action_desc}),'
|
||||||
|
f'组长ID={yonghuid},操作人={operator}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- 3. 提现限额修改(需要6600d)----------
|
||||||
|
kaioi_ewai = request.data.get('kaioi_ewai_tixian')
|
||||||
|
ewai_val = request.data.get('ewai_tixian_xiane')
|
||||||
|
if kaioi_ewai is not None or ewai_val is not None:
|
||||||
|
if '6600d' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改提现限额(需要6600d)'})
|
||||||
|
limit_changes = []
|
||||||
|
old_kaioi = zuzhang.kaioi_ewai_tixian
|
||||||
|
old_ewai = zuzhang.ewai_tixian_xiane
|
||||||
|
if kaioi_ewai is not None:
|
||||||
|
zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai)
|
||||||
|
if bool(kaioi_ewai) != old_kaioi:
|
||||||
|
limit_changes.append(f'额外限额开关:{"开" if old_kaioi else "关"}→{"开" if kaioi_ewai else "关"}')
|
||||||
|
if ewai_val is not None:
|
||||||
|
try:
|
||||||
|
zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val))
|
||||||
|
if zuzhang.ewai_tixian_xiane != old_ewai:
|
||||||
|
limit_changes.append(f'额外限额金额:{old_ewai}→{zuzhang.ewai_tixian_xiane}')
|
||||||
|
except:
|
||||||
|
return Response({'code': 400, 'msg': '额外限额金额格式错误'})
|
||||||
|
zuzhang.save()
|
||||||
|
if limit_changes:
|
||||||
|
write_xiugai_log(
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
xiugaiid=operator,
|
||||||
|
leixing=XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
qitashuoming=(
|
||||||
|
f'【后台】修改组长提现限额:{";".join(limit_changes)},'
|
||||||
|
f'组长ID={yonghuid},操作人={operator}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------- 4. 分红相关修改(需要6600e)----------
|
||||||
|
perm_enabled = request.data.get('permanent_enabled')
|
||||||
|
perm_amount = request.data.get('permanent_amount')
|
||||||
|
custom_first = request.data.get('custom_first_dividend')
|
||||||
|
extra_map = request.data.get('extra_dividend_map')
|
||||||
|
|
||||||
|
if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
|
||||||
|
if '6600e' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限修改分红策略(需要6600e)'})
|
||||||
|
|
||||||
|
# 4.1 永久分红(额外分红开关和金额)
|
||||||
|
if perm_enabled is not None:
|
||||||
|
zuzhang.kaioi_ewai_fenhong = bool(perm_enabled)
|
||||||
|
if perm_amount is not None:
|
||||||
|
try:
|
||||||
|
zuzhang.ewai_fenhong_jine = Decimal(str(perm_amount))
|
||||||
|
except:
|
||||||
|
return Response({'code': 400, 'msg': '永久分红金额格式错误'})
|
||||||
|
zuzhang.save()
|
||||||
|
|
||||||
|
# 4.2 首次定制分红(cishu=1)
|
||||||
|
if custom_first is not None and isinstance(custom_first, dict):
|
||||||
|
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||||
|
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||||
|
existing_first = DuociFenhong.query.filter(
|
||||||
|
club_id=zuzhang_club, yonghuid=yonghuid, cishu=1,
|
||||||
|
)
|
||||||
|
for huiyuan_id, amount in custom_first.items():
|
||||||
|
try:
|
||||||
|
amount = Decimal(str(amount))
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
if amount <= 0:
|
||||||
|
# 删除定制(恢复默认)
|
||||||
|
DuociFenhong.query.filter(
|
||||||
|
club_id=zuzhang_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
|
||||||
|
).delete()
|
||||||
|
else:
|
||||||
|
DuociFenhong.query.update_or_create(
|
||||||
|
club_id=zuzhang_club,
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
huiyuan=huiyuan_id,
|
||||||
|
cishu=1,
|
||||||
|
defaults={
|
||||||
|
'zuzhang_fenhong': amount,
|
||||||
|
'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
|
||||||
|
for record in existing_first:
|
||||||
|
if record.huiyuan not in custom_first:
|
||||||
|
record.delete()
|
||||||
|
|
||||||
|
# 4.3 额外次数分红(cishu >= 2)
|
||||||
|
if extra_map is not None and isinstance(extra_map, dict):
|
||||||
|
from jituan.services.club_penalty import resolve_gsfenhong_club_id
|
||||||
|
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
|
||||||
|
keep_set = set()
|
||||||
|
for times_str, members in extra_map.items():
|
||||||
|
try:
|
||||||
|
cishu = int(times_str)
|
||||||
|
if cishu < 2:
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
if not isinstance(members, dict):
|
||||||
|
continue
|
||||||
|
for huiyuan_id, data in members.items():
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
continue
|
||||||
|
amount = data.get('zuzhang_fenhong')
|
||||||
|
if amount is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
amount = Decimal(str(amount))
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
keep_set.add((cishu, huiyuan_id))
|
||||||
|
DuociFenhong.query.update_or_create(
|
||||||
|
club_id=zuzhang_club,
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
huiyuan=huiyuan_id,
|
||||||
|
cishu=cishu,
|
||||||
|
defaults={
|
||||||
|
'zuzhang_fenhong': amount,
|
||||||
|
'guanshi_fenhong': Decimal('0')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# 删除不在 keep_set 中的额外次数记录
|
||||||
|
existing_extra = DuociFenhong.query.filter(
|
||||||
|
club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2,
|
||||||
|
)
|
||||||
|
for record in existing_extra:
|
||||||
|
if (record.cishu, record.huiyuan) not in keep_set:
|
||||||
|
record.delete()
|
||||||
|
|
||||||
|
dividend_parts = []
|
||||||
|
if perm_enabled is not None:
|
||||||
|
dividend_parts.append(f'永久分红开关→{"开" if perm_enabled else "关"}')
|
||||||
|
if perm_amount is not None:
|
||||||
|
dividend_parts.append(f'永久分红金额→{perm_amount}')
|
||||||
|
if custom_first is not None:
|
||||||
|
dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)')
|
||||||
|
if extra_map is not None:
|
||||||
|
dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)')
|
||||||
|
write_xiugai_log(
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
xiugaiid=operator,
|
||||||
|
leixing=XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
qitashuoming=(
|
||||||
|
f'【后台】修改组长分红策略:{";".join(dividend_parts) or "配置已更新"},'
|
||||||
|
f'组长ID={yonghuid},操作人={operator}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存组长其他可能修改的字段(例如电话、微信没有,忽略)
|
||||||
|
|
||||||
|
return Response({'code': 0, 'msg': '修改成功'})
|
||||||
|
|
||||||
|
|
||||||
|
class ChangeInviterView(APIView):
|
||||||
|
"""
|
||||||
|
后台更换邀请人/上级
|
||||||
|
POST /houtai/htghyqr
|
||||||
|
参数:
|
||||||
|
username: 操作人账号
|
||||||
|
yonghuid: 被修改用户ID
|
||||||
|
target_type: dashou(打手)| guanshi(管事)
|
||||||
|
new_yaoqingren: 新邀请人用户ID(须在 User 主表存在)
|
||||||
|
权限:
|
||||||
|
dashou → 001ee
|
||||||
|
guanshi → 4400a
|
||||||
|
校验:
|
||||||
|
打手新上级须为管事;管事新上级须为组长;不可设为自己
|
||||||
|
"""
|
||||||
|
permission_classes = []
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
username = request.data.get('username', '').strip()
|
||||||
|
yonghuid = request.data.get('yonghuid', '').strip()
|
||||||
|
target_type = request.data.get('target_type', '').strip().lower()
|
||||||
|
new_yaoqingren = request.data.get('new_yaoqingren', '').strip()
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return Response({'code': 401, 'msg': '缺少username'})
|
||||||
|
if not yonghuid:
|
||||||
|
return Response({'code': 400, 'msg': '缺少用户ID'})
|
||||||
|
if target_type not in ('dashou', 'guanshi'):
|
||||||
|
return Response({'code': 400, 'msg': 'target_type 须为 dashou 或 guanshi'})
|
||||||
|
if not new_yaoqingren:
|
||||||
|
return Response({'code': 400, 'msg': '缺少新邀请人ID'})
|
||||||
|
if new_yaoqingren == yonghuid:
|
||||||
|
return Response({'code': 400, 'msg': '不能将自己设为邀请人'})
|
||||||
|
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
if target_type == 'dashou':
|
||||||
|
if '001ee' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限更换打手邀请人(需要001ee)'})
|
||||||
|
else:
|
||||||
|
if '4400a' not in permissions:
|
||||||
|
return Response({'code': 403, 'msg': '无权限更换管事邀请人(需要4400a)'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
target_user = User.query.get(UserUID=yonghuid)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '被修改用户不存在'})
|
||||||
|
|
||||||
|
try:
|
||||||
|
inviter_user = User.query.get(UserUID=new_yaoqingren)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '新邀请人用户ID不存在'})
|
||||||
|
|
||||||
|
operator = kefu.user.Phone
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
if target_type == 'dashou':
|
||||||
|
try:
|
||||||
|
dashou = target_user.DashouProfile
|
||||||
|
except UserDashou.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '用户不是打手'})
|
||||||
|
try:
|
||||||
|
guanshi_profile = inviter_user.GuanshiProfile
|
||||||
|
except UserGuanshi.DoesNotExist:
|
||||||
|
return Response({'code': 400, 'msg': '新邀请人不是管事,无法作为打手上级'})
|
||||||
|
if guanshi_profile.zhuangtai != 1:
|
||||||
|
return Response({'code': 400, 'msg': '新邀请人管事账号已禁用'})
|
||||||
|
old_inviter = dashou.yaoqingren or '无'
|
||||||
|
dashou.yaoqingren = new_yaoqingren
|
||||||
|
dashou.save(update_fields=['yaoqingren'])
|
||||||
|
write_xiugai_log(
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
xiugaiid=operator,
|
||||||
|
leixing=XIUGAI_LEIXING_DASHOU,
|
||||||
|
qitashuoming=(
|
||||||
|
f'【后台】更换打手邀请人:{old_inviter} → {new_yaoqingren},'
|
||||||
|
f'打手ID={yonghuid},新上级须为管事(已校验),操作人={operator}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
guanshi = target_user.GuanshiProfile
|
||||||
|
except UserGuanshi.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '用户不是管事'})
|
||||||
|
try:
|
||||||
|
zuzhang_profile = inviter_user.ZuzhangProfile
|
||||||
|
except UserZuzhang.DoesNotExist:
|
||||||
|
return Response({'code': 400, 'msg': '新邀请人不是组长,无法作为管事上级'})
|
||||||
|
if zuzhang_profile.zhuangtai != 1:
|
||||||
|
return Response({'code': 400, 'msg': '新邀请人组长账号已禁用'})
|
||||||
|
old_inviter = guanshi.yaoqingren or '无'
|
||||||
|
guanshi.yaoqingren = new_yaoqingren
|
||||||
|
guanshi.save(update_fields=['yaoqingren'])
|
||||||
|
write_xiugai_log(
|
||||||
|
yonghuid=yonghuid,
|
||||||
|
xiugaiid=operator,
|
||||||
|
leixing=XIUGAI_LEIXING_GUANSHI,
|
||||||
|
qitashuoming=(
|
||||||
|
f'【后台】更换管事邀请人:{old_inviter} → {new_yaoqingren},'
|
||||||
|
f'管事ID={yonghuid},新上级须为组长(已校验),操作人={operator}'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': '邀请人更换成功',
|
||||||
|
'data': {'yonghuid': yonghuid, 'new_yaoqingren': new_yaoqingren},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
347
backend/views/zxkf.py
Normal file
347
backend/views/zxkf.py
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
import hmac
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import uuid
|
||||||
|
import hashlib
|
||||||
|
import xmltodict
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from calendar import monthrange
|
||||||
|
from decimal import Decimal
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db import models, transaction, IntegrityError
|
||||||
|
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||||||
|
from django.contrib.auth.hashers import make_password
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# 工具类导入
|
||||||
|
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||||||
|
from backend.utils import (
|
||||||
|
update_dashou_daily_by_action,
|
||||||
|
update_shangjia_daily
|
||||||
|
)
|
||||||
|
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||||||
|
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||||||
|
from jituan.services.club_penalty import (
|
||||||
|
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||||||
|
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||||||
|
)
|
||||||
|
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||||||
|
from shop.utils import update_dianpu_daily_stat
|
||||||
|
from products.utils import update_shangpin_daily_stat
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
from ..utils import (
|
||||||
|
verify_kefu_permission,
|
||||||
|
PERMISSION_TO_SHENFEN,
|
||||||
|
SHENFEN_PROFILE_MAP,
|
||||||
|
has_fadan_view_permission,
|
||||||
|
has_merchant_order_permission,
|
||||||
|
check_fadan_permission,
|
||||||
|
pick_order_id,
|
||||||
|
pick_penalty_create_params,
|
||||||
|
write_xiugai_log,
|
||||||
|
XIUGAI_LEIXING_DASHOU,
|
||||||
|
XIUGAI_LEIXING_GUANSHI,
|
||||||
|
XIUGAI_LEIXING_SHANGJIA,
|
||||||
|
XIUGAI_LEIXING_ZUZHANG,
|
||||||
|
XIUGAI_LEIXING_LABEL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# models 集中导入
|
||||||
|
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||||||
|
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||||||
|
## backend
|
||||||
|
from backend.models import WithdrawalDailyStats
|
||||||
|
|
||||||
|
## users
|
||||||
|
from users.models import (
|
||||||
|
UserGuanshi, UserBoss, UserZuzhang,
|
||||||
|
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
## orders
|
||||||
|
from orders.models import (
|
||||||
|
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||||||
|
OrderPlayerHistory, Order, RefundRecord,
|
||||||
|
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||||||
|
)
|
||||||
|
|
||||||
|
## shop
|
||||||
|
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||||||
|
|
||||||
|
## products
|
||||||
|
from products.models import (
|
||||||
|
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||||||
|
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||||||
|
)
|
||||||
|
|
||||||
|
## config
|
||||||
|
from config.models import (
|
||||||
|
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||||||
|
TixianQuotaDefault,
|
||||||
|
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||||||
|
)
|
||||||
|
|
||||||
|
## rank
|
||||||
|
from rank.models import (
|
||||||
|
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||||||
|
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||||||
|
)
|
||||||
|
|
||||||
|
# 序列化器
|
||||||
|
from ..serializers import PopupPageSerializer
|
||||||
|
|
||||||
|
# 全局常量、日志对象
|
||||||
|
logger = logging.getLogger('houtai')
|
||||||
|
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||||||
|
|
||||||
|
class ZxkfghdsView(APIView):
|
||||||
|
"""
|
||||||
|
客服更换打手接口
|
||||||
|
路径:POST /houtai/zxkfghds
|
||||||
|
功能:
|
||||||
|
1. 将当前订单状态改为已退款(5),释放接单打手(若有)
|
||||||
|
2. 如需跨平台通知,则调用对方平台退款通知接口
|
||||||
|
3. 基于原订单信息生成全新订单(状态直接为 1,已替老板填写)
|
||||||
|
4. 生成新的派单链接,并标记为已使用
|
||||||
|
5. 新链接记录上一个链接ID和上一个订单ID
|
||||||
|
6. 【关键】不操作商家余额、退款次数、每日统计,因新订单是替换而非新增扣款
|
||||||
|
权限:002ac(商家订单管理)/ 002ab(平台订单管理)/ 003aa(跨平台管理)
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# ========== 1. 参数提取 ==========
|
||||||
|
username = request.data.get('username', '').strip() or request.data.get('phone', '').strip()
|
||||||
|
OrderID = pick_order_id(request.data)
|
||||||
|
if not username or not OrderID:
|
||||||
|
logger.warning("更换打手参数不完整")
|
||||||
|
return Response({'code': 400, 'msg': '参数不完整'})
|
||||||
|
|
||||||
|
# ========== 2. 权限校验(商家链接页客服需 002ac,非仅跨平台 003aa) ==========
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return permissions # 返回 error response
|
||||||
|
if not has_merchant_order_permission(permissions):
|
||||||
|
return Response({'code': 403, 'msg': '您无权处理订单'})
|
||||||
|
|
||||||
|
current_user = request.user
|
||||||
|
|
||||||
|
# ========== 3. 查询原订单及关联信息 ==========
|
||||||
|
try:
|
||||||
|
old_order = Order.query.select_related('shangjia_kuozhan').get(OrderID=OrderID)
|
||||||
|
except Order.DoesNotExist:
|
||||||
|
logger.warning(f"订单不存在: {OrderID}")
|
||||||
|
return Response({'code': 404, 'msg': '订单不存在'})
|
||||||
|
|
||||||
|
# 仅允许状态为 2(进行中)或 8(结算中)更换打手
|
||||||
|
if old_order.Status not in [1,7,2, 8]:
|
||||||
|
return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'})
|
||||||
|
|
||||||
|
# 本接口仅处理商家发单(Platform=2)
|
||||||
|
if old_order.Platform != 2:
|
||||||
|
return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'})
|
||||||
|
|
||||||
|
# 获取原链接(用于模板ID等)
|
||||||
|
try:
|
||||||
|
old_lianjie = ShangjiaLianjie.query.get(OrderID=OrderID)
|
||||||
|
except ShangjiaLianjie.DoesNotExist:
|
||||||
|
logger.error(f"原链接记录不存在,订单: {OrderID}")
|
||||||
|
return Response({'code': 500, 'msg': '链接数据异常'})
|
||||||
|
|
||||||
|
# 商家信息(仅用于记录关联,不操作余额)
|
||||||
|
try:
|
||||||
|
shangjia_ext = old_order.shangjia_kuozhan
|
||||||
|
shangjia_id = shangjia_ext.MerchantID
|
||||||
|
# 获取商家昵称用于新订单扩展表
|
||||||
|
shangjia_user = User.query.get(UserUID=shangjia_id)
|
||||||
|
shangjia_nicheng = shangjia_user.ShopProfile.nicheng or f"商家{shangjia_id}"
|
||||||
|
except (ObjectDoesNotExist, UserShangjia.DoesNotExist):
|
||||||
|
logger.error(f"商家信息缺失,订单: {OrderID}")
|
||||||
|
return Response({'code': 500, 'msg': '商家数据异常'})
|
||||||
|
|
||||||
|
# ========== 4. 退款处理(仅打手侧,不碰商家) ==========
|
||||||
|
with transaction.atomic():
|
||||||
|
# 5.1 订单状态改为已退款
|
||||||
|
old_order.Status = 5
|
||||||
|
old_order.AssignedCS = current_user.Phone
|
||||||
|
old_order.save()
|
||||||
|
|
||||||
|
# 5.2 处理接单打手(若有)
|
||||||
|
dashou_id = old_order.PlayerID
|
||||||
|
if dashou_id:
|
||||||
|
try:
|
||||||
|
dashou_user = User.query.get(UserUID=dashou_id)
|
||||||
|
dashou_profile = dashou_user.DashouProfile
|
||||||
|
dashou_profile.tuikuanliang += 1 # 退款次数+1
|
||||||
|
dashou_profile.zhuangtai = 1 # 设为空闲
|
||||||
|
dashou_profile.save()
|
||||||
|
except (User.DoesNotExist, ObjectDoesNotExist):
|
||||||
|
logger.warning(f"打手 {dashou_id} 不存在,跳过更新")
|
||||||
|
|
||||||
|
# 5.3 更新退款记录表
|
||||||
|
RefundRecord.query.update_or_create(
|
||||||
|
OrderID=OrderID,
|
||||||
|
defaults={
|
||||||
|
'PlayerID': dashou_id or '',
|
||||||
|
'ApplicantID': '',
|
||||||
|
'ProcessorID': current_user.Phone,
|
||||||
|
'Reason': '客服更换打手退款',
|
||||||
|
'ApplyStatus': 1,
|
||||||
|
'Amount': old_order.Amount or 0,
|
||||||
|
'PlayerCommission': old_order.PlayerCommission or 0,
|
||||||
|
'Description': old_order.Description or '',
|
||||||
|
'Remark': '更换打手自动退款',
|
||||||
|
'Nickname': old_order.Nickname or '',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5.4 打手每日统计(退款)
|
||||||
|
if dashou_id:
|
||||||
|
try:
|
||||||
|
update_dashou_daily_by_action(
|
||||||
|
yonghuid=dashou_id,
|
||||||
|
amount=old_order.PlayerCommission,
|
||||||
|
action=3 # 3=退款
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"打手每日统计更新失败: {e}")
|
||||||
|
|
||||||
|
# 5.5 客服处理统计(管理员无 KefuProfile,跳过)
|
||||||
|
if hasattr(kefu, 'jinrichuli'):
|
||||||
|
kefu.jinrichuli += 1
|
||||||
|
kefu.jinyuechuli += 1
|
||||||
|
kefu.zongchuli += 1
|
||||||
|
kefu.save()
|
||||||
|
|
||||||
|
# 至此,原订单已退款,事务提交
|
||||||
|
|
||||||
|
# ========== 6. 生成新订单和链接(不扣商家余额) ==========
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
# ---- 6.1 生成新的订单ID和Token ----
|
||||||
|
def generate_OrderID():
|
||||||
|
timestamp = int(time.time() * 1000)
|
||||||
|
process_id = os.getpid() % 1000
|
||||||
|
random_num = random.randint(1000, 9999)
|
||||||
|
return f"SJ{timestamp}{process_id:03d}{random_num}"
|
||||||
|
|
||||||
|
def generate_secure_token(dd_id, uid):
|
||||||
|
timestamp = int(time.time() * 1_000_000)
|
||||||
|
salt = secrets.token_hex(8)
|
||||||
|
raw = f"{timestamp}:{dd_id}:{uid}:{salt}"
|
||||||
|
token = hashlib.sha256(raw.encode()).hexdigest()[:24]
|
||||||
|
if ShangjiaLianjie.query.filter(LinkToken=token).exists():
|
||||||
|
return generate_secure_token(dd_id, uid)
|
||||||
|
return token
|
||||||
|
|
||||||
|
def generate_link_url(token):
|
||||||
|
base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/')
|
||||||
|
return f"{base}/order/{token}"
|
||||||
|
|
||||||
|
new_OrderID = generate_OrderID()
|
||||||
|
secure_token = generate_secure_token(new_OrderID, shangjia_id)
|
||||||
|
|
||||||
|
# ---- 6.2 创建新订单(复制核心数据,状态直接为1) ----
|
||||||
|
new_order = Order.query.create(
|
||||||
|
OrderID=new_OrderID,
|
||||||
|
Status=1, # 下单中
|
||||||
|
Platform=2,
|
||||||
|
Amount=old_order.Amount,
|
||||||
|
PlayerCommission=old_order.PlayerCommission,
|
||||||
|
ProductTypeID=old_order.ProductTypeID,
|
||||||
|
GrabRequirement=old_order.GrabRequirement,
|
||||||
|
MembershipID=old_order.MembershipID,
|
||||||
|
CommissionReq=old_order.CommissionReq,
|
||||||
|
Description=old_order.Description,
|
||||||
|
Nickname=old_order.Nickname, # 继承游戏昵称
|
||||||
|
Remark=old_order.Remark,
|
||||||
|
User1ID=old_order.User1ID, # 商家标识
|
||||||
|
ImageURL=old_order.ImageURL,
|
||||||
|
ExternalOrderID=old_order.ExternalOrderID,
|
||||||
|
ClubID=getattr(old_order, 'ClubID', None) or 'xq',
|
||||||
|
# 注意:不继承 AssignedID、PlayerID
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- 6.3 创建商家扩展表 ----
|
||||||
|
MerchantOrderExt.query.create(
|
||||||
|
Order=new_order,
|
||||||
|
MerchantID=shangjia_id,
|
||||||
|
MerchantNickname=shangjia_nicheng,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- 6.4 生成新链接并关联旧链接信息 ----
|
||||||
|
link_url = generate_link_url(secure_token)
|
||||||
|
new_lianjie = ShangjiaLianjie.query.create(
|
||||||
|
UserID=shangjia_id,
|
||||||
|
ProductTypeID=old_lianjie.ProductTypeID,
|
||||||
|
TemplateID=old_lianjie.TemplateID,
|
||||||
|
LinkURL=link_url,
|
||||||
|
LinkToken=secure_token,
|
||||||
|
OrderID=new_OrderID,
|
||||||
|
is_used=True, # 直接标记已使用
|
||||||
|
ExpireTime=timezone.now() + timezone.timedelta(days=1),
|
||||||
|
PreviousLinkID=old_lianjie.id, # 记录旧链接ID
|
||||||
|
PreviousOrderID=old_order.OrderID, # 记录旧订单ID
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---- 6.5 异步通知(广播新订单、跨平台同步) ----
|
||||||
|
try:
|
||||||
|
order_info = {
|
||||||
|
'OrderID': new_OrderID,
|
||||||
|
'game_type': self._get_game_type_name(new_order.ProductTypeID),
|
||||||
|
'amount': str(new_order.Amount),
|
||||||
|
'order_desc': (new_order.Description or '')[:50],
|
||||||
|
}
|
||||||
|
dingdan_guangbo.delay(order_info)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"广播通知失败: {e}")
|
||||||
|
|
||||||
|
# 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单
|
||||||
|
|
||||||
|
logger.info(f"更换打手成功:旧订单{OrderID}→退款,新订单{new_OrderID}已生成")
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': '更换成功',
|
||||||
|
'data': {
|
||||||
|
'new_OrderID': new_OrderID,
|
||||||
|
'new_dingdan_id': new_OrderID,
|
||||||
|
'new_lianjie': link_url,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"生成新订单事务失败: {str(e)}", exc_info=True)
|
||||||
|
return Response({'code': 500, 'msg': '生成新订单失败'})
|
||||||
|
|
||||||
|
def _get_game_type_name(self, ProductTypeID):
|
||||||
|
"""根据类型ID获取游戏类型名称"""
|
||||||
|
try:
|
||||||
|
gt = ShangpinLeixing.query.filter(id=ProductTypeID).first()
|
||||||
|
return gt.jieshao if gt else '游戏订单'
|
||||||
|
except Exception:
|
||||||
|
return '游戏订单'
|
||||||
|
|
||||||
|
|
||||||
27
check_prod_uid.py
Normal file
27
check_prod_uid.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"""检查生产库中 UserUID 为空的用户"""
|
||||||
|
import pymysql
|
||||||
|
|
||||||
|
conn = pymysql.connect(
|
||||||
|
host='gvsds.com', port=50030,
|
||||||
|
user='root', password='sajksh.sdfGH3YUge.wjkd+',
|
||||||
|
database='xaio_cheng_xu', charset='utf8mb4'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# UserUID 为 NULL 的用户数
|
||||||
|
cur.execute("SELECT COUNT(*) FROM user WHERE UserUID IS NULL OR UserUID = ''")
|
||||||
|
print(f'UserUID为空的用户数: {cur.fetchone()[0]}')
|
||||||
|
|
||||||
|
# 总用户数
|
||||||
|
cur.execute("SELECT COUNT(*) FROM user")
|
||||||
|
print(f'总用户数: {cur.fetchone()[0]}')
|
||||||
|
|
||||||
|
# 抽样 UserUID 为 NULL 的用户
|
||||||
|
cur.execute("SELECT UserUUID, UserName, OpenID, Phone FROM user WHERE UserUID IS NULL OR UserUID = '' LIMIT 5")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
if rows:
|
||||||
|
print('\nUserUID为空的用户示例:')
|
||||||
|
for r in rows:
|
||||||
|
print(f' UserName={r[1]}, OpenID={r[2]}, Phone={r[3]}')
|
||||||
|
|
||||||
|
conn.close()
|
||||||
@@ -1 +1,16 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import MiniappScriptScene, MiniappScriptAutoReply
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(MiniappScriptScene)
|
||||||
|
class MiniappScriptSceneAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('club_id', 'scene_key', 'title', 'enabled', 'UpdateTime')
|
||||||
|
list_filter = ('club_id', 'enabled')
|
||||||
|
search_fields = ('scene_key', 'title', 'content')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(MiniappScriptAutoReply)
|
||||||
|
class MiniappScriptAutoReplyAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('club_id', 'keyword', 'reply_type', 'sort_order', 'enabled', 'UpdateTime')
|
||||||
|
list_filter = ('club_id', 'reply_type', 'enabled')
|
||||||
|
search_fields = ('keyword', 'reply_content')
|
||||||
|
|||||||
114
config/management/commands/fix_income_month.py
Normal file
114
config/management/commands/fix_income_month.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
"""
|
||||||
|
修复 repair_wechat_income 搞乱的收入(200 显示成 600+)。
|
||||||
|
|
||||||
|
两种模式(先预览,再加 --yes):
|
||||||
|
|
||||||
|
A. 默认:重扫本月真实微信支付,删掉脏数据后重建
|
||||||
|
python manage.py fix_income_month
|
||||||
|
python manage.py fix_income_month --yes
|
||||||
|
|
||||||
|
B. 若日志金额已对、只是日统计表脏了:
|
||||||
|
python manage.py fix_income_month --logs-only --yes
|
||||||
|
"""
|
||||||
|
from calendar import monthrange
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
from config.models import DailyIncomeStat, PlatformIncomeLog
|
||||||
|
from jituan.services.szjilu_accounting import (
|
||||||
|
align_daily_income_from_logs_only,
|
||||||
|
dedupe_platform_income_logs,
|
||||||
|
fix_income_period,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '修复本月收入统计错乱(repair 重复记账)'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--year', type=int, default=0, help='年,默认今年')
|
||||||
|
parser.add_argument('--month', type=int, default=0, help='月,默认本月')
|
||||||
|
parser.add_argument('--today-only', action='store_true', help='只修今天')
|
||||||
|
parser.add_argument('--logs-only', action='store_true', help='只按现有日志重写日统计,不重扫订单')
|
||||||
|
parser.add_argument('--yes', action='store_true', help='确认执行')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
today = date.today()
|
||||||
|
year = options['year'] or today.year
|
||||||
|
month = options['month'] or today.month
|
||||||
|
dry_run = not options['yes']
|
||||||
|
|
||||||
|
if options['today_only']:
|
||||||
|
start = end = today
|
||||||
|
label = f'今日 {today}'
|
||||||
|
else:
|
||||||
|
_, last = monthrange(year, month)
|
||||||
|
start = date(year, month, 1)
|
||||||
|
end = date(year, month, last)
|
||||||
|
label = f'{year}-{month:02d} 全月'
|
||||||
|
|
||||||
|
if options['logs_only']:
|
||||||
|
self._run_logs_only(dry_run, label)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stdout.write(self.style.WARNING(f'【{label}】收入修复\n'))
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write('预览模式(未改库)\n')
|
||||||
|
|
||||||
|
r = fix_income_period(start, end, dry_run=dry_run)
|
||||||
|
self.stdout.write(f' 错乱日统计合计: {r["before_daily_amount"]:.2f} 元 / {r["before_daily_count"]} 笔')
|
||||||
|
self.stdout.write(f' 错乱日志合计: {r["before_log_amount"]:.2f} 元 / {r["before_log_count"]} 笔')
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f' 重建后(严格): {r["after_amount"]:.2f} 元 / {r["after_count"]} 笔'
|
||||||
|
))
|
||||||
|
|
||||||
|
if r['items']:
|
||||||
|
self.stdout.write(' 明细:')
|
||||||
|
for row in r['items']:
|
||||||
|
if len(row) == 5:
|
||||||
|
d, ref, cid, amt, kind = row
|
||||||
|
self.stdout.write(f' {d} {ref} {amt}元 club={cid} ({kind})')
|
||||||
|
else:
|
||||||
|
ref, cid, amt, kind = row
|
||||||
|
self.stdout.write(f' {ref} {amt}元 club={cid} ({kind})')
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
self._print_today_total(today)
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
'\n重建后金额对 → python manage.py fix_income_month --yes\n'
|
||||||
|
'只修今天 → python manage.py fix_income_month --today-only --yes\n'
|
||||||
|
'日志已对、日统计脏 → python manage.py fix_income_month --logs-only --yes'
|
||||||
|
))
|
||||||
|
|
||||||
|
def _run_logs_only(self, dry_run, label):
|
||||||
|
try:
|
||||||
|
log_sum = PlatformIncomeLog.objects.aggregate(t=Sum('amount'))['t'] or 0
|
||||||
|
log_cnt = PlatformIncomeLog.objects.count()
|
||||||
|
daily_sum = DailyIncomeStat.objects.aggregate(t=Sum('total_amount'))['t'] or 0
|
||||||
|
except Exception as exc:
|
||||||
|
self.stderr.write(f'读取失败: {exc}')
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stdout.write(f'【{label}】仅按日志对齐日统计\n')
|
||||||
|
self.stdout.write(f' platform_income_log 合计: {float(log_sum):.2f} 元 / {log_cnt} 笔')
|
||||||
|
self.stdout.write(f' daily_income_stat 现合计: {float(daily_sum):.2f} 元')
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(self.style.WARNING('\n预览。确认后: python manage.py fix_income_month --logs-only --yes'))
|
||||||
|
return
|
||||||
|
|
||||||
|
n = align_daily_income_from_logs_only()
|
||||||
|
self.stdout.write(self.style.SUCCESS(f'已重写 daily_income_stat {n} 行,并重算 szjilu'))
|
||||||
|
self._print_today_total(date.today())
|
||||||
|
|
||||||
|
def _print_today_total(self, today):
|
||||||
|
agg = DailyIncomeStat.objects.filter(date=today).aggregate(
|
||||||
|
amt=Sum('total_amount'), cnt=Sum('total_count'),
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f'\n数据库今日日统计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)} 笔'
|
||||||
|
))
|
||||||
|
self.stdout.write('请刷新后台财务页核对。')
|
||||||
29
config/management/commands/fix_today_income_count.py
Normal file
29
config/management/commands/fix_today_income_count.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
"""只修正今日 daily_income_stat.total_count(不动金额、不动其它天)。"""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from jituan.services.szjilu_accounting import fix_income_stat_count_for_date
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '修正今日收入笔数(repair 会把 total_count 虚增,本命令只改笔数列)'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--date', type=str, default='', help='YYYY-MM-DD,默认今天')
|
||||||
|
parser.add_argument('--yes', action='store_true')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
stat_date = date.fromisoformat(options['date']) if options['date'] else date.today()
|
||||||
|
dry_run = not options['yes']
|
||||||
|
r = fix_income_stat_count_for_date(stat_date, dry_run=dry_run)
|
||||||
|
self.stdout.write(f'日期: {r["date"]}')
|
||||||
|
self.stdout.write(f' 表内笔数合计(错): {r["before_count"]}')
|
||||||
|
self.stdout.write(self.style.SUCCESS(f' 修正后笔数: {r["after_count"]}'))
|
||||||
|
if r['clubs']:
|
||||||
|
for cid, cnt in r['clubs'].items():
|
||||||
|
self.stdout.write(f' club={cid}: {cnt} 笔')
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(self.style.WARNING('\n确认: python manage.py fix_today_income_count --yes'))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.SUCCESS('\n已写入 daily_income_stat.total_count,请刷新财务页。'))
|
||||||
70
config/management/commands/rebuild_income_stats.py
Normal file
70
config/management/commands/rebuild_income_stats.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
【终极恢复】清空全部收支脏数据,按真实微信支付逐笔重建。
|
||||||
|
|
||||||
|
只执行这一条,不要再用 repair / undo / fix:
|
||||||
|
|
||||||
|
预览:python manage.py rebuild_income_stats
|
||||||
|
执行:python manage.py rebuild_income_stats --yes
|
||||||
|
"""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
from config.models import DailyIncomeStat
|
||||||
|
from jituan.services.szjilu_accounting import rebuild_income_stats_from_scratch
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '清空收支脏数据,按真实微信支付逐笔重建(终极恢复)'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--since-year', type=int, default=0, help='从哪一年扫,默认去年')
|
||||||
|
parser.add_argument('--until-year', type=int, default=0, help='到哪一年,默认今年')
|
||||||
|
parser.add_argument('--yes', action='store_true', help='确认执行')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
until_year = options['until_year'] or date.today().year
|
||||||
|
since_year = options['since_year'] or (until_year - 1)
|
||||||
|
dry_run = not options['yes']
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(self.style.WARNING('【预览】未改数据库\n'))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.ERROR(
|
||||||
|
'【执行】删除全部收入日志和日统计,按真实支付重建...\n'
|
||||||
|
))
|
||||||
|
|
||||||
|
r = rebuild_income_stats_from_scratch(
|
||||||
|
since_year=since_year, until_year=until_year, dry_run=dry_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(f'=== {since_year} ~ {until_year} ===')
|
||||||
|
self.stdout.write(f' 错乱前 daily 合计: {r["before_daily_total"]:.2f} 元')
|
||||||
|
self.stdout.write(f' 错乱前 log: {r["before_log_total"]:.2f} 元 / {r["before_log_count"]} 条')
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f' 重建后合计: {r["after_total"]:.2f} 元 / {r["after_count"]} 笔'
|
||||||
|
))
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f' 重建后本月: {r["month_amount"]:.2f} 元'
|
||||||
|
))
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f' 重建后今日: {r["today_amount"]:.2f} 元 / {r["today_count"]} 笔 ← 看这条'
|
||||||
|
))
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f'\ndaily 表合计: {r.get("after_daily_total", 0):.2f} 元'
|
||||||
|
))
|
||||||
|
agg = DailyIncomeStat.objects.filter(date=date.today()).aggregate(
|
||||||
|
amt=Sum('total_amount'), cnt=Sum('total_count'),
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
f'今日 daily 行合计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)} 笔'
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS('\n完成。刷新后台财务页。'))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
f'\n今日 {r["today_amount"]:.2f} 元对的话:\n'
|
||||||
|
f' python manage.py rebuild_income_stats --yes'
|
||||||
|
))
|
||||||
104
config/management/commands/recover_config_0006.py
Normal file
104
config/management/commands/recover_config_0006.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
"""修复 config.0006 半路失败:去重 + 补唯一约束/索引,然后 fake 0006。"""
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import connection
|
||||||
|
from django.db.models import Max
|
||||||
|
|
||||||
|
from config.models import PopupPage, Tupianpeizhi
|
||||||
|
|
||||||
|
|
||||||
|
def _table_indexes(table):
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(f'SHOW INDEX FROM `{table}`')
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
# Key_name, Column_name, Non_unique
|
||||||
|
by_key = {}
|
||||||
|
for row in rows:
|
||||||
|
key_name = row[2]
|
||||||
|
col = row[4]
|
||||||
|
non_unique = row[1]
|
||||||
|
by_key.setdefault(key_name, {'cols': [], 'unique': non_unique == 0})
|
||||||
|
by_key[key_name]['cols'].append(col)
|
||||||
|
return by_key
|
||||||
|
|
||||||
|
|
||||||
|
def _has_unique_on(table, columns):
|
||||||
|
cols = list(columns)
|
||||||
|
for info in _table_indexes(table).values():
|
||||||
|
if info['unique'] and info['cols'] == cols:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_index(table, key_name):
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(f'ALTER TABLE `{table}` DROP INDEX `{key_name}`')
|
||||||
|
|
||||||
|
|
||||||
|
def _add_unique(table, name, columns):
|
||||||
|
cols = ', '.join(f'`{c}`' for c in columns)
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(f'ALTER TABLE `{table}` ADD UNIQUE `{name}` ({cols})')
|
||||||
|
|
||||||
|
|
||||||
|
def _add_index(table, name, columns):
|
||||||
|
cols = ', '.join(f'`{c}`' for c in columns)
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(f'CREATE INDEX `{name}` ON `{table}` ({cols})')
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'config.0006 半路失败时:清重复数据、补约束,再执行 migrate config 0006 --fake'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.stdout.write('1) 清理重复数据…')
|
||||||
|
|
||||||
|
for row in Tupianpeizhi.objects.values('club_id', 'ImageType').annotate(max_id=Max('id')):
|
||||||
|
n = Tupianpeizhi.objects.filter(
|
||||||
|
club_id=row['club_id'],
|
||||||
|
ImageType=row['ImageType'],
|
||||||
|
).exclude(id=row['max_id']).delete()[0]
|
||||||
|
if n:
|
||||||
|
self.stdout.write(f' tupianpeizhi 删除 {n} 条重复')
|
||||||
|
|
||||||
|
for row in PopupPage.objects.values('club_id', 'page_key').annotate(max_id=Max('id')):
|
||||||
|
n = PopupPage.objects.filter(
|
||||||
|
club_id=row['club_id'],
|
||||||
|
page_key=row['page_key'],
|
||||||
|
).exclude(id=row['max_id']).delete()[0]
|
||||||
|
if n:
|
||||||
|
self.stdout.write(f' popup_page 删除 {n} 条重复')
|
||||||
|
|
||||||
|
self.stdout.write('2) 检查并补约束/索引…')
|
||||||
|
|
||||||
|
# popup_page:去掉 page_key 单独唯一,改为 (club_id, page_key)
|
||||||
|
popup_indexes = _table_indexes('popup_page')
|
||||||
|
for key_name, info in popup_indexes.items():
|
||||||
|
if info['unique'] and info['cols'] == ['page_key']:
|
||||||
|
self.stdout.write(f' 删除 popup_page 旧唯一索引 {key_name}')
|
||||||
|
_drop_index('popup_page', key_name)
|
||||||
|
|
||||||
|
if not _has_unique_on('popup_page', ['club_id', 'page_key']):
|
||||||
|
self.stdout.write(' 添加 popup_page UNIQUE (club_id, page_key)')
|
||||||
|
_add_unique('popup_page', 'popup_page_club_page_key_uniq', ['club_id', 'page_key'])
|
||||||
|
else:
|
||||||
|
self.stdout.write(' popup_page UNIQUE (club_id, page_key) 已存在')
|
||||||
|
|
||||||
|
if not _has_unique_on('tupianpeizhi', ['club_id', 'ImageType']):
|
||||||
|
self.stdout.write(' 添加 tupianpeizhi UNIQUE (club_id, ImageType)')
|
||||||
|
_add_unique('tupianpeizhi', 'tupianpeizhi_club_id_ImageType_uniq', ['club_id', 'ImageType'])
|
||||||
|
else:
|
||||||
|
self.stdout.write(' tupianpeizhi UNIQUE (club_id, ImageType) 已存在')
|
||||||
|
|
||||||
|
lunbo_indexes = _table_indexes('lunbo')
|
||||||
|
if 'lunbo_club_page_type_idx' not in lunbo_indexes:
|
||||||
|
self.stdout.write(' 添加 lunbo 索引 lunbo_club_page_type_idx')
|
||||||
|
_add_index('lunbo', 'lunbo_club_page_type_idx', ['club_id', 'page_key', 'ImageType'])
|
||||||
|
else:
|
||||||
|
self.stdout.write(' lunbo 索引 lunbo_club_page_type_idx 已存在')
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
'\n完成。请执行:\n'
|
||||||
|
' python manage.py migrate config 0006 --fake\n'
|
||||||
|
' python manage.py migrate config\n'
|
||||||
|
' python manage.py migrate\n'
|
||||||
|
))
|
||||||
92
config/management/commands/recover_wechat_income_stats.py
Normal file
92
config/management/commands/recover_wechat_income_stats.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
"""
|
||||||
|
恢复被 repair_wechat_income 搞乱的收入统计。
|
||||||
|
|
||||||
|
repair 会对已入账订单重复加钱(200 变 900)。
|
||||||
|
本命令:删当日错误日志 → 按真实微信支付重建 → 扣回 szjilu 多记部分。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
1. 先预览:python manage.py recover_wechat_income_stats
|
||||||
|
2. 看「重建后」金额是否对(应接近真实收入,如 200 左右)
|
||||||
|
3. 确认后:python manage.py recover_wechat_income_stats --yes
|
||||||
|
4. 若多日错乱:python manage.py recover_wechat_income_stats --days 7 --yes
|
||||||
|
5. 最后全量对齐日统计表:python manage.py recover_wechat_income_stats --yes --rebuild-all-daily
|
||||||
|
"""
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
from config.models import DailyIncomeStat
|
||||||
|
from jituan.services.szjilu_accounting import (
|
||||||
|
rebuild_all_daily_income_from_logs,
|
||||||
|
recover_income_stats_for_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '恢复 repair_wechat_income 重复记账(先预览,--yes 执行)'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--date', type=str, default='', help='日期 YYYY-MM-DD,默认今天')
|
||||||
|
parser.add_argument('--days', type=int, default=1, help='向前恢复几天')
|
||||||
|
parser.add_argument('--yes', action='store_true', help='确认执行')
|
||||||
|
parser.add_argument(
|
||||||
|
'--rebuild-all-daily',
|
||||||
|
action='store_true',
|
||||||
|
help='恢复后按 platform_income_log 全量重写 daily_income_stat(纠正累计总收入)',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
if options['date']:
|
||||||
|
end_day = date.fromisoformat(options['date'])
|
||||||
|
else:
|
||||||
|
end_day = date.today()
|
||||||
|
days = max(1, int(options['days'] or 1))
|
||||||
|
start_day = end_day - timedelta(days=days - 1)
|
||||||
|
dry_run = not options['yes']
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
'【预览】未改数据库。下面「重建后」应是正确金额。\n'
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING('【执行】正在恢复...\n'))
|
||||||
|
|
||||||
|
for i in range(days):
|
||||||
|
d = start_day + timedelta(days=i)
|
||||||
|
r = recover_income_stats_for_date(d, dry_run=dry_run)
|
||||||
|
self.stdout.write(f'=== {r["date"]} ===')
|
||||||
|
self.stdout.write(
|
||||||
|
f' 错乱前日统计: {r["before_daily_amount"]:.2f} 元 / {r["before_daily_count"]} 笔'
|
||||||
|
)
|
||||||
|
self.stdout.write(
|
||||||
|
f' 当日幂等日志: {r["before_log_amount"]:.2f} 元 / {r["before_log_count"]} 笔'
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f' 重建后(正确值): {r["after_amount"]:.2f} 元 / {r["after_count"]} 笔'
|
||||||
|
))
|
||||||
|
if r['items']:
|
||||||
|
self.stdout.write(' 明细:')
|
||||||
|
for ref, cid, amt, kind in r['items']:
|
||||||
|
self.stdout.write(f' {ref} {amt}元 club={cid} ({kind})')
|
||||||
|
|
||||||
|
if not dry_run and options['rebuild_all_daily']:
|
||||||
|
n = rebuild_all_daily_income_from_logs()
|
||||||
|
self.stdout.write(self.style.SUCCESS(f'已按日志全量重写 daily_income_stat: {n} 行'))
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
agg = DailyIncomeStat.objects.filter(date=end_day).aggregate(
|
||||||
|
amt=Sum('total_amount'),
|
||||||
|
cnt=Sum('total_count'),
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f'\n数据库今日日统计现为: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)} 笔'
|
||||||
|
))
|
||||||
|
self.stdout.write(self.style.SUCCESS('恢复完成,请刷新后台财务页。'))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
'\n预览完成。重建后金额对的话执行:\n'
|
||||||
|
' python manage.py recover_wechat_income_stats --yes\n'
|
||||||
|
'累计总收入仍不对再加:\n'
|
||||||
|
' python manage.py recover_wechat_income_stats --yes --rebuild-all-daily'
|
||||||
|
))
|
||||||
15
config/management/commands/repair_wechat_income.py
Normal file
15
config/management/commands/repair_wechat_income.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"""【已废弃】请用 sync_daily_income_from_log,本命令曾导致重复记账。"""
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '已废弃:勿使用。请改用 python manage.py sync_daily_income_from_log --reset-day'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.stderr.write(self.style.ERROR(
|
||||||
|
'repair_wechat_income 已禁用(会对已入账订单重复加钱)。\n'
|
||||||
|
'纠正今日虚高请执行:\n'
|
||||||
|
' python manage.py sync_daily_income_from_log --reset-day\n'
|
||||||
|
'可选同时重算累计:\n'
|
||||||
|
' python manage.py sync_daily_income_from_log --reset-day --sync-szjilu'
|
||||||
|
))
|
||||||
48
config/management/commands/repopulate_today_income.py
Normal file
48
config/management/commands/repopulate_today_income.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"""只恢复今日收入(修复后台四个板子显示 0)。"""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
from config.models import DailyIncomeStat
|
||||||
|
from jituan.services.szjilu_accounting import repopulate_today_income_stats
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '重建今日 daily_income_stat(修复财务页今日收入为 0)'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--date', type=str, default='', help='日期 YYYY-MM-DD,默认今天')
|
||||||
|
parser.add_argument('--yes', action='store_true', help='确认执行')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
stat_date = date.fromisoformat(options['date']) if options['date'] else date.today()
|
||||||
|
dry_run = not options['yes']
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(self.style.WARNING('【预览】\n'))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING('【执行】重建今日收入...\n'))
|
||||||
|
|
||||||
|
r = repopulate_today_income_stats(stat_date, dry_run=dry_run)
|
||||||
|
self.stdout.write(f'日期: {r["date"]}')
|
||||||
|
self.stdout.write(f' 当前: {r["before_amount"]:.2f} 元 / {r["before_count"]} 笔')
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f' 重建后: {r["after_amount"]:.2f} 元 / {r["after_count"]} 笔'
|
||||||
|
))
|
||||||
|
if r['items']:
|
||||||
|
for ref, cid, amt, kind in r['items']:
|
||||||
|
self.stdout.write(f' {ref} {amt}元 club={cid} ({kind})')
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
agg = DailyIncomeStat.objects.filter(date=stat_date).aggregate(
|
||||||
|
amt=Sum('total_amount'), cnt=Sum('total_count'),
|
||||||
|
)
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f'\n完成。今日日统计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)} 笔'
|
||||||
|
))
|
||||||
|
self.stdout.write('请刷新后台财务页。')
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
'\n确认后: python manage.py repopulate_today_income --yes'
|
||||||
|
))
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""
|
||||||
|
从订单/充值表恢复 daily_income_stat(不删订单,只重算统计表)。
|
||||||
|
repair/rebuild 搞乱后用这个,不要用 repair_wechat_income。
|
||||||
|
|
||||||
|
预览:python manage.py restore_daily_income_from_orders
|
||||||
|
执行:python manage.py restore_daily_income_from_orders --yes
|
||||||
|
"""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from jituan.services.szjilu_accounting import restore_daily_income_from_orders
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '从订单/充值表恢复日收入统计(订单不动)'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--since-year', type=int, default=0)
|
||||||
|
parser.add_argument('--until-year', type=int, default=0)
|
||||||
|
parser.add_argument('--yes', action='store_true')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
until_year = options['until_year'] or date.today().year
|
||||||
|
since_year = options['since_year'] or (until_year - 1)
|
||||||
|
dry_run = not options['yes']
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(self.style.WARNING('【预览】订单数据不会动\n'))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING('【执行】按订单/充值重算日统计...\n'))
|
||||||
|
|
||||||
|
r = restore_daily_income_from_orders(since_year, until_year, dry_run=dry_run)
|
||||||
|
self.stdout.write(f'范围: {r["since_year"]} ~ {r["until_year"]}')
|
||||||
|
self.stdout.write(f' 支付笔数(订单表): {r["payment_rows"]}')
|
||||||
|
self.stdout.write(f' 日统计行数: {r["daily_rows"]}')
|
||||||
|
self.stdout.write(f' 当前 daily 合计: {r["before_daily_total"]:.2f} 元')
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f' 恢复后 daily 合计: {r["after_daily_total"]:.2f} 元'
|
||||||
|
))
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
'\n确认: python manage.py restore_daily_income_from_orders --yes'
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.SUCCESS('\n完成。推 kefu 后刷新财务页。'))
|
||||||
68
config/management/commands/restore_finance_stats.py
Normal file
68
config/management/commands/restore_finance_stats.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
"""
|
||||||
|
恢复 daily_income_stat(每日收入统计表)全表数据。
|
||||||
|
|
||||||
|
repair_wechat_income 对该表的破坏:
|
||||||
|
- 把扫描到的每笔订单/充值重复写入 daily_income_stat
|
||||||
|
- 金额记到「执行命令那天」,不是真实支付日
|
||||||
|
- 含退款单(Status=5)也被当成收入加进去
|
||||||
|
|
||||||
|
本命令按 platform_income_log + 无日志的已支付单,重建整张 daily_income_stat。
|
||||||
|
不碰 daily_payout_stat,不碰 szjilu,不删订单。
|
||||||
|
|
||||||
|
预览:python manage.py restore_finance_stats
|
||||||
|
执行:python manage.py restore_finance_stats --yes
|
||||||
|
"""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
from config.models import DailyIncomeStat, DailyPayoutStat
|
||||||
|
from jituan.services.szjilu_accounting import rebuild_daily_income_stat_table
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '恢复 daily_income_stat 全表(财务四个板子读这张表)'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('--yes', action='store_true', help='确认写入数据库')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
dry_run = not options['yes']
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(self.style.WARNING('【预览】未改数据库\n'))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING('【执行】重建 daily_income_stat...\n'))
|
||||||
|
|
||||||
|
r = rebuild_daily_income_stat_table(dry_run=dry_run)
|
||||||
|
|
||||||
|
self.stdout.write(f' 支付笔数(去重): {r["payment_rows"]}')
|
||||||
|
self.stdout.write(f' 其中日志: {r["from_log"]}')
|
||||||
|
self.stdout.write(f' 补漏订单: {r["gap_orders"]}')
|
||||||
|
self.stdout.write(f' 补漏充值: {r["gap_cz"]}')
|
||||||
|
self.stdout.write(f' 日统计行数: {r["daily_rows"]}')
|
||||||
|
self.stdout.write(f' 恢复前 daily 合计: {r["before_daily_total"]:.2f} 元')
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f' 恢复后 daily 合计: {r["after_daily_total"]:.2f} 元'
|
||||||
|
))
|
||||||
|
self.stdout.write(f' 其中今日收入: {r["today_amount"]:.2f} 元 / {r["today_count"]} 笔')
|
||||||
|
|
||||||
|
payout_today = DailyPayoutStat.objects.filter(date=date.today()).aggregate(
|
||||||
|
t=Sum('total_amount'),
|
||||||
|
)['t'] or 0
|
||||||
|
self.stdout.write(
|
||||||
|
f' (未动)今日支出表: {float(payout_today):.2f} 元'
|
||||||
|
)
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
'\n预览数字合理后执行:\n'
|
||||||
|
' python manage.py restore_finance_stats --yes\n'
|
||||||
|
'然后 push 代码并刷新财务页。'
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
check = DailyIncomeStat.objects.aggregate(t=Sum('total_amount'))['t'] or 0
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f'\n完成。数据库 daily_income_stat 合计: {float(check):.2f} 元'
|
||||||
|
))
|
||||||
68
config/management/commands/sync_daily_income_from_log.py
Normal file
68
config/management/commands/sync_daily_income_from_log.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
"""
|
||||||
|
用 platform_income_log 重写 daily_income_stat,纠正重复记账后的虚高数字。
|
||||||
|
权威数据源是 platform_income_log(每笔支付一条,biz_ref 唯一),不是扫订单重算。
|
||||||
|
"""
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from jituan.services.szjilu_accounting import (
|
||||||
|
sync_daily_income_stat_for_date,
|
||||||
|
sync_szjilu_income_from_logs,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = (
|
||||||
|
'按 platform_income_log 同步 daily_income_stat(纠正今日收入虚高/笔数不准)。'
|
||||||
|
'不要用 repair_wechat_income,那个会重复加钱。'
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--date',
|
||||||
|
type=str,
|
||||||
|
default='',
|
||||||
|
help='同步日期 YYYY-MM-DD,默认今天',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--days',
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help='从指定日期起向前同步几天,默认 1',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--reset-day',
|
||||||
|
action='store_true',
|
||||||
|
help='同步前先删除该日全部日统计行,再按日志重建(纠正重复记账必选)',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--sync-szjilu',
|
||||||
|
action='store_true',
|
||||||
|
help='同时按日志全量重算 szjilu 累计收入/今日流水',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
if options['date']:
|
||||||
|
end_day = date.fromisoformat(options['date'])
|
||||||
|
else:
|
||||||
|
end_day = date.today()
|
||||||
|
days = max(1, int(options['days'] or 1))
|
||||||
|
start_day = end_day - timedelta(days=days - 1)
|
||||||
|
reset_day = bool(options['reset_day'])
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
for i in range(days):
|
||||||
|
d = start_day + timedelta(days=i)
|
||||||
|
n = sync_daily_income_stat_for_date(d, reset_day=reset_day)
|
||||||
|
total += n
|
||||||
|
self.stdout.write(f'{d}: 已同步 {n} 个俱乐部日统计行')
|
||||||
|
|
||||||
|
if options['sync_szjilu']:
|
||||||
|
sz = sync_szjilu_income_from_logs()
|
||||||
|
self.stdout.write(f'szjilu: 已重算 {sz} 个俱乐部累计收入')
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f'完成。今日收入/笔数现与 platform_income_log 一致。'
|
||||||
|
f'若仍有漏记支付,请确认 migrate config 后等新支付自动入账。'
|
||||||
|
))
|
||||||
133
config/management/commands/undo_repair_wechat_income.py
Normal file
133
config/management/commands/undo_repair_wechat_income.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
"""
|
||||||
|
【反向撤销】repair_wechat_income 造成的重复记账。
|
||||||
|
|
||||||
|
你当时执行的指令是:
|
||||||
|
python manage.py repair_wechat_income
|
||||||
|
或:
|
||||||
|
python manage.py repair_wechat_income --date 2026-06-24 --days 7
|
||||||
|
|
||||||
|
repair 逻辑:按「当日创建的已支付订单/充值单」逐条 +金额、+笔数、写 platform_income_log、加 szjilu。
|
||||||
|
本命令用完全相同扫描列表,逐条反向 -金额、-笔数、删日志、减 szjilu。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
1. 预览(不改库):
|
||||||
|
python manage.py undo_repair_wechat_income
|
||||||
|
|
||||||
|
2. 确认后撤销(加多少减多少):
|
||||||
|
python manage.py undo_repair_wechat_income --yes
|
||||||
|
|
||||||
|
3. 若当时带了参数,必须一致:
|
||||||
|
python manage.py undo_repair_wechat_income --date 2026-06-01 --days 30 --run-date 2026-06-24 --yes
|
||||||
|
(--run-date = 你执行 repair 那天,默认今天)
|
||||||
|
|
||||||
|
4. 全部清零(恢复到 0):
|
||||||
|
python manage.py undo_repair_wechat_income --zero-all --yes
|
||||||
|
"""
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
from config.models import DailyIncomeStat
|
||||||
|
from jituan.services.szjilu_accounting import (
|
||||||
|
undo_repair_wechat_income_period,
|
||||||
|
zero_all_income_statistics,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '反向撤销 repair_wechat_income 造成的收支重复记账'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--date', type=str, default='',
|
||||||
|
help='与 repair 相同的 --date(扫描区间结束日),默认今天',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--days', type=int, default=1,
|
||||||
|
help='与 repair 相同的 --days,默认 1',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--run-date', type=str, default='',
|
||||||
|
help='你执行 repair 的日期(金额被记到哪天的 daily),默认今天',
|
||||||
|
)
|
||||||
|
parser.add_argument('--yes', action='store_true', help='确认执行')
|
||||||
|
parser.add_argument(
|
||||||
|
'--zero-all', action='store_true',
|
||||||
|
help='收支统计全部清零(日收入、日志、szjilu 收入流水归零)',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
if options['zero_all']:
|
||||||
|
self._zero_all(not options['yes'])
|
||||||
|
return
|
||||||
|
|
||||||
|
if options['date']:
|
||||||
|
end_day = date.fromisoformat(options['date'])
|
||||||
|
else:
|
||||||
|
end_day = date.today()
|
||||||
|
days = max(1, int(options['days'] or 1))
|
||||||
|
start_day = end_day - timedelta(days=days - 1)
|
||||||
|
if options['run_date']:
|
||||||
|
inflation_date = date.fromisoformat(options['run_date'])
|
||||||
|
else:
|
||||||
|
inflation_date = date.today()
|
||||||
|
|
||||||
|
dry_run = not options['yes']
|
||||||
|
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
'========== 反向撤销 repair_wechat_income ==========\n'
|
||||||
|
f'原 repair 扫描区间: {start_day} ~ {end_day} (--days {days})\n'
|
||||||
|
f'金额被记到日统计日期: {inflation_date} (--run-date)\n'
|
||||||
|
))
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write('【预览】未改数据库\n')
|
||||||
|
else:
|
||||||
|
self.stdout.write('【执行】正在反向扣减...\n')
|
||||||
|
|
||||||
|
r = undo_repair_wechat_income_period(
|
||||||
|
start_day, end_day, inflation_date, dry_run=dry_run,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stdout.write(f' 将撤销条数: {r["undo_count"]} 条')
|
||||||
|
self.stdout.write(f' 将扣减金额: {r["undo_amount"]:.2f} 元')
|
||||||
|
self.stdout.write(
|
||||||
|
f' 执行前 {inflation_date} 日统计: {r["before_daily_on_inflation_date"]:.2f} 元'
|
||||||
|
)
|
||||||
|
|
||||||
|
if r['items']:
|
||||||
|
self.stdout.write(' 明细:')
|
||||||
|
for ref, cid, amt, kind in r['items']:
|
||||||
|
self.stdout.write(f' - {ref} {amt}元 club={cid} ({kind})')
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f'\n执行后 {inflation_date} 日统计: {r.get("after_daily_on_inflation_date", 0):.2f} 元'
|
||||||
|
))
|
||||||
|
self.stdout.write(self.style.SUCCESS('反向撤销完成,请刷新后台财务页。'))
|
||||||
|
else:
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
'\n预览完成。确认后执行(参数与 repair 保持一致):\n'
|
||||||
|
f' python manage.py undo_repair_wechat_income --date {end_day} --days {days} '
|
||||||
|
f'--run-date {inflation_date} --yes\n'
|
||||||
|
'全部清零:\n'
|
||||||
|
' python manage.py undo_repair_wechat_income --zero-all --yes'
|
||||||
|
))
|
||||||
|
|
||||||
|
def _zero_all(self, dry_run):
|
||||||
|
agg = DailyIncomeStat.objects.aggregate(amt=Sum('total_amount'), cnt=Sum('total_count'))
|
||||||
|
self.stdout.write(self.style.ERROR(
|
||||||
|
'========== 收支统计全部清零 ==========\n'
|
||||||
|
f'当前 daily_income_stat 合计: {float(agg["amt"] or 0):.2f} 元 / {int(agg["cnt"] or 0)} 笔\n'
|
||||||
|
'将删除: platform_income_log 全部、daily_income_stat 全部\n'
|
||||||
|
'将归零: szjilu.TotalIncome / TotalFlow / DailyFlow\n'
|
||||||
|
))
|
||||||
|
if dry_run:
|
||||||
|
self.stdout.write(self.style.WARNING(
|
||||||
|
'预览。确认清零:\n'
|
||||||
|
' python manage.py undo_repair_wechat_income --zero-all --yes'
|
||||||
|
))
|
||||||
|
return
|
||||||
|
zero_all_income_statistics()
|
||||||
|
self.stdout.write(self.style.SUCCESS('已全部清零。请刷新后台。'))
|
||||||
@@ -1,92 +1,296 @@
|
|||||||
# Generated by Django 5.2.10 on 2026-01-14 19:59
|
# Generated by Django 4.2.27 on 2026-06-17 21:18
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
initial = True
|
|
||||||
|
initial = True
|
||||||
dependencies = [
|
|
||||||
]
|
dependencies = [
|
||||||
|
]
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
operations = [
|
||||||
name='Gonggao',
|
migrations.CreateModel(
|
||||||
fields=[
|
name='Gonggao',
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
fields=[
|
||||||
('leixing', models.IntegerField(blank=True, null=True, verbose_name='公告类型')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
('leixing', models.IntegerField(blank=True, null=True, verbose_name='公告类型')),
|
||||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
('jieshao', models.TextField(blank=True, null=True, verbose_name='公告介绍')),
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||||
],
|
('jieshao', models.TextField(blank=True, null=True, verbose_name='公告介绍')),
|
||||||
options={
|
],
|
||||||
'verbose_name': '公告',
|
options={
|
||||||
'verbose_name_plural': '公告',
|
'verbose_name': '公告',
|
||||||
'db_table': 'gonggao',
|
'verbose_name_plural': '公告',
|
||||||
},
|
'db_table': 'gonggao',
|
||||||
),
|
},
|
||||||
migrations.CreateModel(
|
),
|
||||||
name='Lunbo',
|
migrations.CreateModel(
|
||||||
fields=[
|
name='Lunbo',
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
fields=[
|
||||||
('tupian_url', models.CharField(blank=True, max_length=500, null=True, verbose_name='轮播图片URL')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('leixing', models.IntegerField(blank=True, null=True, verbose_name='图片类型:1:轮播2:个人中心背景')),
|
('tupian_url', models.CharField(blank=True, max_length=500, null=True, verbose_name='轮播图片URL')),
|
||||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
('leixing', models.IntegerField(blank=True, null=True, verbose_name='图片类型:1:轮播2:个人中心背景')),
|
||||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
],
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||||
options={
|
],
|
||||||
'verbose_name': '轮播',
|
options={
|
||||||
'verbose_name_plural': '轮播',
|
'verbose_name': '轮播',
|
||||||
'db_table': 'lunbo',
|
'verbose_name_plural': '轮播',
|
||||||
},
|
'db_table': 'lunbo',
|
||||||
),
|
},
|
||||||
migrations.CreateModel(
|
),
|
||||||
name='Qunpeizhi',
|
migrations.CreateModel(
|
||||||
fields=[
|
name='PopupConfig',
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
fields=[
|
||||||
('peizhiid', models.IntegerField(blank=True, null=True, verbose_name='群类型:1.打手 2.管事')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
('popup_id', models.CharField(max_length=50, verbose_name='弹窗唯一ID')),
|
||||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
('title', models.CharField(blank=True, max_length=200, verbose_name='标题(后台用)')),
|
||||||
('neirong', models.CharField(blank=True, max_length=200, null=True, verbose_name='群介绍')),
|
('description', models.TextField(blank=True, verbose_name='描述(后台用)')),
|
||||||
('qunid', models.CharField(blank=True, max_length=100, null=True, verbose_name='群id')),
|
('strategy_type', models.CharField(choices=[('once', '仅一次'), ('daily', '每日'), ('weekly', '每周'), ('monthly', '每月'), ('yearly', '每年'), ('always', '始终'), ('count', '周期内次数限制')], default='once', max_length=20)),
|
||||||
('jieshao', models.CharField(blank=True, max_length=200, null=True, verbose_name='介绍')),
|
('max_count', models.PositiveIntegerField(default=1, verbose_name='周期内最大展示次数')),
|
||||||
],
|
('reset_interval', models.CharField(blank=True, choices=[('day', '天'), ('week', '周'), ('month', '月'), ('year', '年')], max_length=20, verbose_name='重置周期(count类型时使用)')),
|
||||||
options={
|
('duration', models.PositiveIntegerField(default=0, verbose_name='强制阅读秒数(0=无强制)')),
|
||||||
'db_table': 'qunpeizhi',
|
('force_even_muted', models.BooleanField(default=False, verbose_name='强制弹出(忽略用户今日屏蔽)')),
|
||||||
},
|
('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')),
|
||||||
),
|
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||||
migrations.CreateModel(
|
('start_time', models.DateTimeField(blank=True, null=True, verbose_name='生效开始时间')),
|
||||||
name='Szjilu',
|
('end_time', models.DateTimeField(blank=True, null=True, verbose_name='生效结束时间')),
|
||||||
fields=[
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='记录ID')),
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
('zongsy', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总收益')),
|
],
|
||||||
('zongls', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总流水')),
|
options={
|
||||||
('zongzc', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总支出')),
|
'verbose_name': '弹窗配置',
|
||||||
('jrzc', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日支出')),
|
'verbose_name_plural': '弹窗配置',
|
||||||
('jrls', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日流水')),
|
'db_table': 'popup_config',
|
||||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
'ordering': ['sort_order', 'id'],
|
||||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
},
|
||||||
],
|
),
|
||||||
options={
|
migrations.CreateModel(
|
||||||
'verbose_name': '收支记录',
|
name='PopupPage',
|
||||||
'verbose_name_plural': '收支记录',
|
fields=[
|
||||||
'db_table': 'szjilu',
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
},
|
('page_key', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='页面标识')),
|
||||||
),
|
('name', models.CharField(max_length=100, verbose_name='页面名称')),
|
||||||
migrations.CreateModel(
|
('description', models.TextField(blank=True, verbose_name='描述')),
|
||||||
name='Tupianpeizhi',
|
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||||
fields=[
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
('tupian', models.CharField(blank=True, max_length=500, null=True, verbose_name='配置图片URL')),
|
('ignore_user_mute', models.BooleanField(default=False, verbose_name='忽略用户今日屏蔽')),
|
||||||
('leixing', models.PositiveIntegerField(blank=True, null=True, verbose_name='提现类型1:打手规则图片2:默认头像图片')),
|
],
|
||||||
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
options={
|
||||||
('update_time', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
'verbose_name': '弹窗页面',
|
||||||
],
|
'verbose_name_plural': '弹窗页面',
|
||||||
options={
|
'db_table': 'popup_page',
|
||||||
'verbose_name': '配置图片',
|
},
|
||||||
'verbose_name_plural': '配置图片',
|
),
|
||||||
'db_table': 'tupianpeizhi',
|
migrations.CreateModel(
|
||||||
},
|
name='Qunpeizhi',
|
||||||
),
|
fields=[
|
||||||
]
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('peizhiid', models.IntegerField(blank=True, null=True, verbose_name='群类型:1.打手 2.管事')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||||
|
('neirong', models.CharField(blank=True, max_length=200, null=True, verbose_name='群介绍')),
|
||||||
|
('qunid', models.CharField(blank=True, max_length=100, null=True, verbose_name='群id')),
|
||||||
|
('jieshao', models.CharField(blank=True, max_length=200, null=True, verbose_name='介绍')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'qunpeizhi',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Szjilu',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='记录ID')),
|
||||||
|
('zongsy', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总收益')),
|
||||||
|
('zongls', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总流水')),
|
||||||
|
('zongzc', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='总支出')),
|
||||||
|
('jrzc', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日支出')),
|
||||||
|
('jrls', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='今日流水')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '收支记录',
|
||||||
|
'verbose_name_plural': '收支记录',
|
||||||
|
'db_table': 'szjilu',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TixianQuotaDefault',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('leixing', models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], unique=True, verbose_name='用户类型')),
|
||||||
|
('default_quota', models.DecimalField(decimal_places=2, default=20.0, max_digits=10, verbose_name='默认每日提现限额(元)')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '默认提现限额',
|
||||||
|
'verbose_name_plural': '默认提现限额',
|
||||||
|
'db_table': 'tixian_quota_default',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Tupianpeizhi',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('tupian', models.CharField(blank=True, max_length=500, null=True, verbose_name='配置图片URL')),
|
||||||
|
('leixing', models.PositiveIntegerField(blank=True, null=True, verbose_name='提现类型1:打手规则图片2:默认头像图片')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更改时间')),
|
||||||
|
('fangwen_cishu', models.PositiveIntegerField(default=0, verbose_name='访问次数')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '配置图片',
|
||||||
|
'verbose_name_plural': '配置图片',
|
||||||
|
'db_table': 'tupianpeizhi',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WithdrawConfig',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('mode', models.IntegerField(choices=[(1, '自动提现'), (2, '手动提现')], default=2, verbose_name='提现模式')),
|
||||||
|
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '提现模式配置',
|
||||||
|
'verbose_name_plural': '提现模式配置',
|
||||||
|
'db_table': 'withdraw_config',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ShangjiaMoban',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='模板ID')),
|
||||||
|
('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
|
||||||
|
('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')),
|
||||||
|
('moban_jieshao', models.CharField(max_length=500, verbose_name='模板介绍')),
|
||||||
|
('jiage', models.DecimalField(decimal_places=2, help_text='单位:元', max_digits=10, verbose_name='模板价格')),
|
||||||
|
('fabu_shuliang', models.PositiveIntegerField(default=0, verbose_name='发布数量')),
|
||||||
|
('yongjin_qiangdan', models.BooleanField(default=False, verbose_name='是否开启佣金抢单')),
|
||||||
|
('yongjin_jiage', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='佣金价格')),
|
||||||
|
('chenghao_id', models.IntegerField(blank=True, null=True, verbose_name='关联称号ID')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '商家订单模板',
|
||||||
|
'verbose_name_plural': '商家订单模板',
|
||||||
|
'db_table': 'shangjia_moban',
|
||||||
|
'indexes': [models.Index(fields=['yonghu_id', 'leixing_id'], name='shangjia_mo_yonghu__bb287d_idx'), models.Index(fields=['yonghu_id', 'leixing_id', 'id'], name='shangjia_mo_yonghu__4240d0_idx')],
|
||||||
|
'unique_together': {('yonghu_id', 'leixing_id', 'moban_jieshao')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ShangjiaLianjie',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='链接ID')),
|
||||||
|
('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
|
||||||
|
('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')),
|
||||||
|
('moban_id', models.PositiveIntegerField(db_index=True, verbose_name='模板ID')),
|
||||||
|
('lianjie', models.CharField(max_length=500, unique=True, verbose_name='链接地址')),
|
||||||
|
('dingdan_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='订单ID')),
|
||||||
|
('is_used', models.BooleanField(db_index=True, default=False, verbose_name='是否已使用')),
|
||||||
|
('shiyongzhe_ip', models.CharField(blank=True, max_length=50, null=True, verbose_name='使用者IP')),
|
||||||
|
('youxiao_shijian', models.DateTimeField(blank=True, null=True, verbose_name='有效截止时间')),
|
||||||
|
('lianjie_token', models.CharField(blank=True, db_index=True, max_length=32, null=True, unique=True, verbose_name='链接Token')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('previous_lianjie_id', models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name='上一个链接ID')),
|
||||||
|
('previous_dingdan_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='上一个订单ID')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '商家链接',
|
||||||
|
'verbose_name_plural': '商家链接',
|
||||||
|
'db_table': 'shangjia_lianjie',
|
||||||
|
'indexes': [models.Index(fields=['yonghu_id', 'moban_id', 'leixing_id'], name='shangjia_li_yonghu__9f2b25_idx'), models.Index(fields=['lianjie_token'], name='shangjia_li_lianjie_99014c_idx'), models.Index(fields=['dingdan_id'], name='shangjia_li_dingdan_95b682_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PopupImage',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('image_url', models.CharField(max_length=500, verbose_name='图片相对路径')),
|
||||||
|
('text', models.TextField(blank=True, verbose_name='图片上方文字')),
|
||||||
|
('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('popup_config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='config.popupconfig')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '弹窗图片',
|
||||||
|
'verbose_name_plural': '弹窗图片',
|
||||||
|
'db_table': 'popup_image',
|
||||||
|
'ordering': ['sort_order', 'id'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='popupconfig',
|
||||||
|
name='page',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='popups', to='config.popuppage'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DailyPayoutStat',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')),
|
||||||
|
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
|
||||||
|
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
|
||||||
|
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='出款总额')),
|
||||||
|
('total_count', models.PositiveIntegerField(default=0, verbose_name='出款总笔数')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '每日出款统计',
|
||||||
|
'verbose_name_plural': '每日出款统计',
|
||||||
|
'db_table': 'daily_payout_stat',
|
||||||
|
'indexes': [models.Index(fields=['date'], name='daily_payou_date_b534ec_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_payou_year_6376ca_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DailyIncomeStat',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')),
|
||||||
|
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
|
||||||
|
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
|
||||||
|
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
|
||||||
|
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='总收入金额')),
|
||||||
|
('total_count', models.PositiveIntegerField(default=0, verbose_name='总收入笔数')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '每日收入统计',
|
||||||
|
'verbose_name_plural': '每日收入统计',
|
||||||
|
'db_table': 'daily_income_stat',
|
||||||
|
'indexes': [models.Index(fields=['date'], name='daily_incom_date_75e6df_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_incom_year_d85d10_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AccountPermission',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('account_id', models.CharField(db_index=True, max_length=11, verbose_name='账号ID')),
|
||||||
|
('permission_code', models.IntegerField(verbose_name='权限代码')),
|
||||||
|
('is_enabled', models.BooleanField(default=False, verbose_name='是否开启')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '账号权限',
|
||||||
|
'verbose_name_plural': '账号权限',
|
||||||
|
'db_table': 'account_permission',
|
||||||
|
'unique_together': {('account_id', 'permission_code')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='popupconfig',
|
||||||
|
unique_together={('page', 'popup_id')},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
64
config/migrations/0002_rename_pinyin_to_pascal.py
Normal file
64
config/migrations/0002_rename_pinyin_to_pascal.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Generated manually to rename pinyin fields to PascalCase
|
||||||
|
# db_column is preserved, so these are no-ops at the database level
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('config', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# Lunbo
|
||||||
|
migrations.RenameField(model_name='lunbo', old_name='tupian_url', new_name='ImageURL'),
|
||||||
|
migrations.RenameField(model_name='lunbo', old_name='leixing', new_name='ImageType'),
|
||||||
|
|
||||||
|
# Gonggao
|
||||||
|
migrations.RenameField(model_name='gonggao', old_name='leixing', new_name='NoticeType'),
|
||||||
|
migrations.RenameField(model_name='gonggao', old_name='jieshao', new_name='Content'),
|
||||||
|
|
||||||
|
# Qunpeizhi
|
||||||
|
migrations.RenameField(model_name='qunpeizhi', old_name='peizhiid', new_name='GroupType'),
|
||||||
|
migrations.RenameField(model_name='qunpeizhi', old_name='neirong', new_name='GroupContent'),
|
||||||
|
migrations.RenameField(model_name='qunpeizhi', old_name='qunid', new_name='GroupID'),
|
||||||
|
migrations.RenameField(model_name='qunpeizhi', old_name='jieshao', new_name='Description'),
|
||||||
|
|
||||||
|
# Tupianpeizhi
|
||||||
|
migrations.RenameField(model_name='tupianpeizhi', old_name='tupian', new_name='ImageURL'),
|
||||||
|
migrations.RenameField(model_name='tupianpeizhi', old_name='leixing', new_name='ImageType'),
|
||||||
|
migrations.RenameField(model_name='tupianpeizhi', old_name='fangwen_cishu', new_name='AccessCount'),
|
||||||
|
|
||||||
|
# Szjilu
|
||||||
|
migrations.RenameField(model_name='szjilu', old_name='zongsy', new_name='TotalIncome'),
|
||||||
|
migrations.RenameField(model_name='szjilu', old_name='zongls', new_name='TotalFlow'),
|
||||||
|
migrations.RenameField(model_name='szjilu', old_name='zongzc', new_name='TotalExpense'),
|
||||||
|
migrations.RenameField(model_name='szjilu', old_name='jrzc', new_name='DailyExpense'),
|
||||||
|
migrations.RenameField(model_name='szjilu', old_name='jrls', new_name='DailyFlow'),
|
||||||
|
|
||||||
|
# ShangjiaMoban
|
||||||
|
migrations.RenameField(model_name='shangjiamoban', old_name='yonghu_id', new_name='UserID'),
|
||||||
|
migrations.RenameField(model_name='shangjiamoban', old_name='leixing_id', new_name='ProductTypeID'),
|
||||||
|
migrations.RenameField(model_name='shangjiamoban', old_name='moban_jieshao', new_name='TemplateDesc'),
|
||||||
|
migrations.RenameField(model_name='shangjiamoban', old_name='jiage', new_name='Price'),
|
||||||
|
migrations.RenameField(model_name='shangjiamoban', old_name='fabu_shuliang', new_name='PublishedCount'),
|
||||||
|
migrations.RenameField(model_name='shangjiamoban', old_name='yongjin_qiangdan', new_name='CommissionGrabEnabled'),
|
||||||
|
migrations.RenameField(model_name='shangjiamoban', old_name='yongjin_jiage', new_name='CommissionPrice'),
|
||||||
|
migrations.RenameField(model_name='shangjiamoban', old_name='chenghao_id', new_name='TitleID'),
|
||||||
|
|
||||||
|
# ShangjiaLianjie
|
||||||
|
migrations.RenameField(model_name='shangjialianjie', old_name='yonghu_id', new_name='UserID'),
|
||||||
|
migrations.RenameField(model_name='shangjialianjie', old_name='leixing_id', new_name='ProductTypeID'),
|
||||||
|
migrations.RenameField(model_name='shangjialianjie', old_name='moban_id', new_name='TemplateID'),
|
||||||
|
migrations.RenameField(model_name='shangjialianjie', old_name='lianjie', new_name='LinkURL'),
|
||||||
|
migrations.RenameField(model_name='shangjialianjie', old_name='dingdan_id', new_name='OrderID'),
|
||||||
|
migrations.RenameField(model_name='shangjialianjie', old_name='shiyongzhe_ip', new_name='UserIP'),
|
||||||
|
migrations.RenameField(model_name='shangjialianjie', old_name='youxiao_shijian', new_name='ExpireTime'),
|
||||||
|
migrations.RenameField(model_name='shangjialianjie', old_name='lianjie_token', new_name='LinkToken'),
|
||||||
|
migrations.RenameField(model_name='shangjialianjie', old_name='previous_lianjie_id', new_name='PreviousLinkID'),
|
||||||
|
migrations.RenameField(model_name='shangjialianjie', old_name='previous_dingdan_id', new_name='PreviousOrderID'),
|
||||||
|
|
||||||
|
# TixianQuotaDefault
|
||||||
|
migrations.RenameField(model_name='tixianquotadefault', old_name='leixing', new_name='UserType'),
|
||||||
|
]
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-01-27 12:18
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('config', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ShangjiaMoban',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='模板ID')),
|
|
||||||
('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
|
|
||||||
('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')),
|
|
||||||
('moban_jieshao', models.CharField(max_length=500, verbose_name='模板介绍')),
|
|
||||||
('jiage', models.DecimalField(decimal_places=2, help_text='单位:元', max_digits=10, verbose_name='模板价格')),
|
|
||||||
('fabu_shuliang', models.PositiveIntegerField(default=0, 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': 'shangjia_moban',
|
|
||||||
'indexes': [models.Index(fields=['yonghu_id', 'leixing_id'], name='shangjia_mo_yonghu__bb287d_idx'), models.Index(fields=['yonghu_id', 'leixing_id', 'id'], name='shangjia_mo_yonghu__4240d0_idx')],
|
|
||||||
'unique_together': {('yonghu_id', 'leixing_id', 'moban_jieshao')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ShangjiaLianjie',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='链接ID')),
|
|
||||||
('yonghu_id', models.CharField(db_index=True, max_length=7, verbose_name='用户ID')),
|
|
||||||
('leixing_id', models.PositiveIntegerField(db_index=True, verbose_name='商品类型ID')),
|
|
||||||
('moban_id', models.PositiveIntegerField(db_index=True, verbose_name='模板ID')),
|
|
||||||
('lianjie', models.CharField(max_length=500, unique=True, verbose_name='链接地址')),
|
|
||||||
('dingdan_id', models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='订单ID')),
|
|
||||||
('is_used', models.BooleanField(db_index=True, default=False, verbose_name='是否已使用')),
|
|
||||||
('shiyongzhe_ip', models.CharField(blank=True, max_length=50, null=True, verbose_name='使用者IP')),
|
|
||||||
('youxiao_shijian', models.DateTimeField(blank=True, null=True, verbose_name='有效截止时间')),
|
|
||||||
('lianjie_token', models.CharField(blank=True, db_index=True, max_length=32, null=True, unique=True, verbose_name='链接Token')),
|
|
||||||
('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': 'shangjia_lianjie',
|
|
||||||
'indexes': [models.Index(fields=['yonghu_id', 'moban_id', 'leixing_id'], name='shangjia_li_yonghu__9f2b25_idx'), models.Index(fields=['lianjie_token'], name='shangjia_li_lianjie_99014c_idx'), models.Index(fields=['dingdan_id'], name='shangjia_li_dingdan_95b682_idx')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Generated by Django 4.2.27 on 2026-06-19 11:36
|
||||||
|
# Modified to use SeparateDatabaseAndState for RemoveIndex to bypass
|
||||||
|
# MySQL backend's _create_missing_fk_index check that fails after RenameField
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('config', '0002_rename_pinyin_to_pascal'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
# RemoveIndex operations wrapped in SeparateDatabaseAndState
|
||||||
|
# to bypass MySQL backend's FK index check that references old field names
|
||||||
|
migrations.SeparateDatabaseAndState(
|
||||||
|
state_operations=[
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name='shangjialianjie',
|
||||||
|
name='shangjia_li_yonghu__9f2b25_idx',
|
||||||
|
),
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name='shangjialianjie',
|
||||||
|
name='shangjia_li_lianjie_99014c_idx',
|
||||||
|
),
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name='shangjialianjie',
|
||||||
|
name='shangjia_li_dingdan_95b682_idx',
|
||||||
|
),
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name='shangjiamoban',
|
||||||
|
name='shangjia_mo_yonghu__bb287d_idx',
|
||||||
|
),
|
||||||
|
migrations.RemoveIndex(
|
||||||
|
model_name='shangjiamoban',
|
||||||
|
name='shangjia_mo_yonghu__4240d0_idx',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
database_operations=[
|
||||||
|
migrations.RunSQL(
|
||||||
|
sql=[
|
||||||
|
'DROP INDEX `shangjia_li_yonghu__9f2b25_idx` ON `shangjia_lianjie`',
|
||||||
|
'DROP INDEX `shangjia_li_lianjie_99014c_idx` ON `shangjia_lianjie`',
|
||||||
|
'DROP INDEX `shangjia_li_dingdan_95b682_idx` ON `shangjia_lianjie`',
|
||||||
|
'DROP INDEX `shangjia_mo_yonghu__bb287d_idx` ON `shangjia_moban`',
|
||||||
|
'DROP INDEX `shangjia_mo_yonghu__4240d0_idx` ON `shangjia_moban`',
|
||||||
|
],
|
||||||
|
reverse_sql=[
|
||||||
|
# No reverse needed - indexes will be recreated by AddIndex
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
# AddIndex operations - these should work fine since fields exist with new names
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='shangjialianjie',
|
||||||
|
index=models.Index(fields=['UserID', 'TemplateID', 'ProductTypeID'], name='shangjia_li_UserID_e59d94_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='shangjialianjie',
|
||||||
|
index=models.Index(fields=['LinkToken'], name='shangjia_li_LinkTok_6b32bb_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='shangjialianjie',
|
||||||
|
index=models.Index(fields=['OrderID'], name='shangjia_li_OrderID_e7ddf9_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='shangjiamoban',
|
||||||
|
index=models.Index(fields=['UserID', 'ProductTypeID'], name='shangjia_mo_UserID_abd699_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='shangjiamoban',
|
||||||
|
index=models.Index(fields=['UserID', 'ProductTypeID', 'id'], name='shangjia_mo_UserID_2dcb54_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-03-16 17:04
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('config', '0002_shangjiamoban_shangjialianjie'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='tupianpeizhi',
|
|
||||||
name='fangwen_cishu',
|
|
||||||
field=models.PositiveIntegerField(default=0, verbose_name='访问次数'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
47
config/migrations/0004_daily_stat_club_id.py
Normal file
47
config/migrations/0004_daily_stat_club_id.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('config', '0003_remove_shangjialianjie_shangjia_li_yonghu__9f2b25_idx_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='dailyincomestat',
|
||||||
|
name='club_id',
|
||||||
|
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='所属俱乐部'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='dailypayoutstat',
|
||||||
|
name='club_id',
|
||||||
|
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='所属俱乐部'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dailyincomestat',
|
||||||
|
name='date',
|
||||||
|
field=models.DateField(db_index=True, verbose_name='统计日期'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dailypayoutstat',
|
||||||
|
name='date',
|
||||||
|
field=models.DateField(db_index=True, verbose_name='统计日期'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='dailyincomestat',
|
||||||
|
index=models.Index(fields=['club_id', 'date'], name='idx_income_club_date'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='dailypayoutstat',
|
||||||
|
index=models.Index(fields=['club_id', 'date'], name='idx_payout_club_date'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='dailyincomestat',
|
||||||
|
constraint=models.UniqueConstraint(fields=('date', 'club_id'), name='uniq_daily_income_club_date'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='dailypayoutstat',
|
||||||
|
constraint=models.UniqueConstraint(fields=('date', 'club_id'), name='uniq_daily_payout_club_date'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-03-20 15:50
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('config', '0003_tupianpeizhi_fangwen_cishu'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='TixianQuotaDefault',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
|
||||||
('leixing', models.IntegerField(choices=[(1, '打手'), (2, '管事')], unique=True, verbose_name='用户类型')),
|
|
||||||
('default_quota', models.DecimalField(decimal_places=2, default=20.0, max_digits=10, 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': 'tixian_quota_default',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-03-24 17:34
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('config', '0004_tixianquotadefault'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='OrderTypeMapping',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('partner_club_id', models.CharField(db_index=True, max_length=6, verbose_name='对方俱乐部ID')),
|
|
||||||
('partner_type_id', models.PositiveIntegerField(verbose_name='对方订单类型ID')),
|
|
||||||
('partner_type_name', models.CharField(max_length=100, verbose_name='对方订单类型名称')),
|
|
||||||
('our_type_id', models.PositiveIntegerField(verbose_name='我方订单类型ID')),
|
|
||||||
('our_type_name', models.CharField(max_length=100, 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': 'order_type_mapping',
|
|
||||||
'unique_together': {('partner_club_id', 'partner_type_id')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='DailyDispatchStat',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('date', models.DateField(db_index=True, verbose_name='统计日期')),
|
|
||||||
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
|
|
||||||
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
|
|
||||||
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
|
|
||||||
('direction', models.PositiveSmallIntegerField(choices=[(1, '我方派单'), (2, '对方派单')], verbose_name='派单方向')),
|
|
||||||
('partner_club_id', models.CharField(db_index=True, max_length=6, verbose_name='对方俱乐部ID')),
|
|
||||||
('dispatch_count', models.PositiveIntegerField(default=0, verbose_name='派单数')),
|
|
||||||
('dispatch_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='派单总额')),
|
|
||||||
('success_count', models.PositiveIntegerField(default=0, verbose_name='成交订单数')),
|
|
||||||
('success_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, 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': 'daily_dispatch_stat',
|
|
||||||
'indexes': [models.Index(fields=['year', 'month', 'day', 'direction', 'partner_club_id'], name='daily_dispa_year_f98f91_idx'), models.Index(fields=['date'], name='daily_dispa_date_9ac7fe_idx')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Club',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('club_id', models.CharField(db_index=True, max_length=6, verbose_name='本俱乐部ID')),
|
|
||||||
('partner_club_id', models.CharField(max_length=6, unique=True, verbose_name='对方俱乐部ID')),
|
|
||||||
('is_interop_enabled', models.BooleanField(default=False, verbose_name='是否开启平台互通')),
|
|
||||||
('is_receive_enabled', models.BooleanField(default=False, verbose_name='是否同意接收对方订单')),
|
|
||||||
('dashou_rate', models.DecimalField(decimal_places=4, default=1.0, max_digits=5, verbose_name='打手分红费率')),
|
|
||||||
('partner_domain', models.URLField(blank=True, 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': 'club',
|
|
||||||
'indexes': [models.Index(fields=['club_id'], name='club_club_id_e75636_idx'), models.Index(fields=['partner_club_id'], name='club_partner_b76039_idx')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
37
config/migrations/0005_szjilu_club_id.py
Normal file
37
config/migrations/0005_szjilu_club_id.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def backfill_szjilu_club_id(apps, schema_editor):
|
||||||
|
Szjilu = apps.get_model('config', 'Szjilu')
|
||||||
|
for row in Szjilu.objects.all():
|
||||||
|
if not getattr(row, 'club_id', None):
|
||||||
|
row.club_id = 'xq'
|
||||||
|
row.save(update_fields=['club_id'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('config', '0004_daily_stat_club_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='szjilu',
|
||||||
|
name='club_id',
|
||||||
|
field=models.CharField(
|
||||||
|
db_index=True,
|
||||||
|
default='xq',
|
||||||
|
max_length=16,
|
||||||
|
verbose_name='俱乐部ID',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(backfill_szjilu_club_id, migrations.RunPython.noop),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='szjilu',
|
||||||
|
constraint=models.UniqueConstraint(
|
||||||
|
fields=('club_id',),
|
||||||
|
name='uniq_szjilu_club_id',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-04-01 09:22
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('config', '0005_ordertypemapping_dailydispatchstat_club'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ClubConfig',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('club_id', models.CharField(db_index=True, max_length=6, verbose_name='俱乐部ID')),
|
|
||||||
('club_nickname', models.CharField(blank=True, default='', max_length=50, verbose_name='俱乐部昵称')),
|
|
||||||
('club_avatar', models.CharField(blank=True, default='', max_length=500, verbose_name='俱乐部头像地址')),
|
|
||||||
('backend_domain', models.CharField(blank=True, default='', max_length=200, verbose_name='后台域名')),
|
|
||||||
('chat_api_key', models.CharField(blank=True, default='', max_length=200, verbose_name='聊天API密钥')),
|
|
||||||
('chat_api_id', models.CharField(blank=True, default='', max_length=200, verbose_name='聊天API ID')),
|
|
||||||
('customer_service_api_url', models.CharField(blank=True, default='', max_length=200, verbose_name='客服API URL')),
|
|
||||||
('customer_service_corp_id', models.CharField(blank=True, default='', max_length=100, verbose_name='客服企业ID')),
|
|
||||||
('storage_bucket_domain', models.CharField(blank=True, default='', max_length=500, verbose_name='存储桶域名')),
|
|
||||||
('is_self', models.SmallIntegerField(choices=[(0, '对方'), (1, '我方')], db_index=True, default=0, 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': 'club_config',
|
|
||||||
'unique_together': {('club_id',)},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='AccountPermission',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('account_id', models.CharField(db_index=True, max_length=11, verbose_name='账号ID')),
|
|
||||||
('permission_code', models.IntegerField(verbose_name='权限代码')),
|
|
||||||
('is_enabled', models.BooleanField(default=False, verbose_name='是否开启')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '账号权限',
|
|
||||||
'verbose_name_plural': '账号权限',
|
|
||||||
'db_table': 'account_permission',
|
|
||||||
'unique_together': {('account_id', 'permission_code')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
81
config/migrations/0006_display_club_id.py
Normal file
81
config/migrations/0006_display_club_id.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.models import Max
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_popuppage(apps, schema_editor):
|
||||||
|
PopupPage = apps.get_model('config', 'PopupPage')
|
||||||
|
for row in PopupPage.objects.values('club_id', 'page_key').annotate(max_id=Max('id')):
|
||||||
|
PopupPage.objects.filter(
|
||||||
|
club_id=row['club_id'],
|
||||||
|
page_key=row['page_key'],
|
||||||
|
).exclude(id=row['max_id']).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_tupianpeizhi(apps, schema_editor):
|
||||||
|
Tupianpeizhi = apps.get_model('config', 'Tupianpeizhi')
|
||||||
|
for row in Tupianpeizhi.objects.values('club_id', 'ImageType').annotate(max_id=Max('id')):
|
||||||
|
Tupianpeizhi.objects.filter(
|
||||||
|
club_id=row['club_id'],
|
||||||
|
ImageType=row['ImageType'],
|
||||||
|
).exclude(id=row['max_id']).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('config', '0005_szjilu_club_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='gonggao',
|
||||||
|
name='club_id',
|
||||||
|
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='lunbo',
|
||||||
|
name='club_id',
|
||||||
|
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='lunbo',
|
||||||
|
name='page_key',
|
||||||
|
field=models.CharField(
|
||||||
|
db_index=True, default='order_pool', max_length=32,
|
||||||
|
verbose_name='页面标识(order_pool/accept_order/merchant_home/dashou_center)',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='popuppage',
|
||||||
|
name='club_id',
|
||||||
|
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='tupianpeizhi',
|
||||||
|
name='club_id',
|
||||||
|
field=models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='popuppage',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='popuppage',
|
||||||
|
name='page_key',
|
||||||
|
field=models.CharField(db_index=True, max_length=50, verbose_name='页面标识'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(_dedupe_popuppage, migrations.RunPython.noop),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='popuppage',
|
||||||
|
unique_together={('club_id', 'page_key')},
|
||||||
|
),
|
||||||
|
migrations.RunPython(_dedupe_tupianpeizhi, migrations.RunPython.noop),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='tupianpeizhi',
|
||||||
|
unique_together={('club_id', 'ImageType')},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='lunbo',
|
||||||
|
index=models.Index(fields=['club_id', 'page_key', 'ImageType'], name='lunbo_club_page_type_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-04-02 13:31
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('config', '0006_clubconfig_accountpermission'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='club',
|
|
||||||
name='receive_rate',
|
|
||||||
field=models.DecimalField(decimal_places=4, default=1.0, max_digits=5, verbose_name='接收打手分红费率'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='dailydispatchstat',
|
|
||||||
name='claimed_amount',
|
|
||||||
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='抢单总额'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='dailydispatchstat',
|
|
||||||
name='claimed_count',
|
|
||||||
field=models.PositiveIntegerField(default=0, verbose_name='抢单数'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
26
config/migrations/0007_platform_income_log.py
Normal file
26
config/migrations/0007_platform_income_log.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('config', '0006_display_club_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PlatformIncomeLog',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('biz_ref', models.CharField(db_index=True, max_length=64, unique=True, verbose_name='业务单号')),
|
||||||
|
('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')),
|
||||||
|
('amount', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='入账金额')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '平台入账幂等日志',
|
||||||
|
'verbose_name_plural': '平台入账幂等日志',
|
||||||
|
'db_table': 'platform_income_log',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-04-04 18:51
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('config', '0007_club_receive_rate_dailydispatchstat_claimed_amount_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='clubconfig',
|
|
||||||
name='storage_bucket_name',
|
|
||||||
field=models.CharField(blank=True, default='', max_length=100, verbose_name='存储桶名称'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
34
config/migrations/0008_gonggao_page_key.py
Normal file
34
config/migrations/0008_gonggao_page_key.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def backfill_gonggao_page_key(apps, schema_editor):
|
||||||
|
Gonggao = apps.get_model('config', 'Gonggao')
|
||||||
|
for row in Gonggao.objects.filter(page_key='').only('id'):
|
||||||
|
row.page_key = 'order_pool'
|
||||||
|
row.save(update_fields=['page_key'])
|
||||||
|
for row in Gonggao.objects.filter(page_key__isnull=True).only('id'):
|
||||||
|
row.page_key = 'order_pool'
|
||||||
|
row.save(update_fields=['page_key'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('config', '0007_platform_income_log'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='gonggao',
|
||||||
|
name='page_key',
|
||||||
|
field=models.CharField(
|
||||||
|
db_index=True, default='order_pool', max_length=32,
|
||||||
|
verbose_name='页面标识(order_pool/accept_order/merchant_home)',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(backfill_gonggao_page_key, migrations.RunPython.noop),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='gonggao',
|
||||||
|
unique_together={('club_id', 'page_key')},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-04-05 13:34
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('config', '0008_clubconfig_storage_bucket_name'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='DailyPayoutStat',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')),
|
|
||||||
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
|
|
||||||
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
|
|
||||||
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
|
|
||||||
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='出款总额')),
|
|
||||||
('total_count', models.PositiveIntegerField(default=0, 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': 'daily_payout_stat',
|
|
||||||
'indexes': [models.Index(fields=['date'], name='daily_payou_date_b534ec_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_payou_year_6376ca_idx')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='DailyIncomeStat',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('date', models.DateField(db_index=True, unique=True, verbose_name='统计日期')),
|
|
||||||
('year', models.PositiveSmallIntegerField(verbose_name='年份')),
|
|
||||||
('month', models.PositiveSmallIntegerField(verbose_name='月份')),
|
|
||||||
('day', models.PositiveSmallIntegerField(verbose_name='日期')),
|
|
||||||
('total_amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='总收入金额')),
|
|
||||||
('total_count', models.PositiveIntegerField(default=0, 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': 'daily_income_stat',
|
|
||||||
'indexes': [models.Index(fields=['date'], name='daily_incom_date_75e6df_idx'), models.Index(fields=['year', 'month', 'day'], name='daily_incom_year_d85d10_idx')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
27
config/migrations/0009_gonggao_copy_pages.py
Normal file
27
config/migrations/0009_gonggao_copy_pages.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def copy_gonggao_to_other_pages(apps, schema_editor):
|
||||||
|
"""将 order_pool 公告复制到 accept_order / merchant_home(仅当目标页尚无记录)。"""
|
||||||
|
Gonggao = apps.get_model('config', 'Gonggao')
|
||||||
|
for pk in ('accept_order', 'merchant_home'):
|
||||||
|
for row in Gonggao.objects.filter(page_key='order_pool'):
|
||||||
|
if Gonggao.objects.filter(club_id=row.club_id, page_key=pk).exists():
|
||||||
|
continue
|
||||||
|
Gonggao.objects.create(
|
||||||
|
club_id=row.club_id,
|
||||||
|
page_key=pk,
|
||||||
|
NoticeType=row.NoticeType,
|
||||||
|
Content=row.Content or '',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('config', '0008_gonggao_page_key'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(copy_gonggao_to_other_pages, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
67
config/migrations/0010_club_miniapp_icon_pindao.py
Normal file
67
config/migrations/0010_club_miniapp_icon_pindao.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('config', '0009_gonggao_copy_pages'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClubMiniappIcon',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')),
|
||||||
|
('icon_key', models.CharField(db_index=True, max_length=64, verbose_name='图标键')),
|
||||||
|
('label', models.CharField(blank=True, default='', max_length=100, verbose_name='展示名称')),
|
||||||
|
('image_path', models.CharField(blank=True, default='', max_length=500, verbose_name='图片相对路径')),
|
||||||
|
('description', models.CharField(blank=True, default='', max_length=300, verbose_name='说明')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '小程序图标',
|
||||||
|
'verbose_name_plural': '小程序图标',
|
||||||
|
'db_table': 'club_miniapp_icon',
|
||||||
|
'ordering': ['icon_key'],
|
||||||
|
'unique_together': {('club_id', 'icon_key')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClubPindao',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('club_id', models.CharField(default='xq', max_length=16, unique=True, verbose_name='俱乐部ID')),
|
||||||
|
('channel_no', models.CharField(blank=True, default='', max_length=100, verbose_name='频道号')),
|
||||||
|
('title', models.CharField(blank=True, default='频道', max_length=100, verbose_name='标题')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '俱乐部频道',
|
||||||
|
'verbose_name_plural': '俱乐部频道',
|
||||||
|
'db_table': 'club_pindao',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ClubPindaoImage',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')),
|
||||||
|
('image_path', models.CharField(max_length=500, verbose_name='图片相对路径')),
|
||||||
|
('sort_order', models.PositiveIntegerField(default=0, verbose_name='排序')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('pindao', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='images', to='config.clubpindao')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '频道图片',
|
||||||
|
'verbose_name_plural': '频道图片',
|
||||||
|
'db_table': 'club_pindao_image',
|
||||||
|
'ordering': ['sort_order', 'id'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-04-22 11:24
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('config', '0009_dailypayoutstat_dailyincomestat'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='PopupConfig',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('popup_id', models.CharField(max_length=50, verbose_name='弹窗唯一ID')),
|
|
||||||
('title', models.CharField(blank=True, max_length=200, verbose_name='标题(后台用)')),
|
|
||||||
('description', models.TextField(blank=True, verbose_name='描述(后台用)')),
|
|
||||||
('strategy_type', models.CharField(choices=[('once', '仅一次'), ('daily', '每日'), ('weekly', '每周'), ('monthly', '每月'), ('yearly', '每年'), ('always', '始终'), ('count', '周期内次数限制')], default='once', max_length=20)),
|
|
||||||
('max_count', models.PositiveIntegerField(default=1, verbose_name='周期内最大展示次数')),
|
|
||||||
('reset_interval', models.CharField(blank=True, choices=[('day', '天'), ('week', '周'), ('month', '月'), ('year', '年')], max_length=20, verbose_name='重置周期(count类型时使用)')),
|
|
||||||
('duration', models.PositiveIntegerField(default=0, verbose_name='强制阅读秒数(0=无强制)')),
|
|
||||||
('force_even_muted', models.BooleanField(default=False, verbose_name='强制弹出(忽略用户今日屏蔽)')),
|
|
||||||
('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')),
|
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
|
||||||
('start_time', models.DateTimeField(blank=True, null=True, verbose_name='生效开始时间')),
|
|
||||||
('end_time', models.DateTimeField(blank=True, null=True, verbose_name='生效结束时间')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '弹窗配置',
|
|
||||||
'verbose_name_plural': '弹窗配置',
|
|
||||||
'db_table': 'popup_config',
|
|
||||||
'ordering': ['sort_order', 'id'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='PopupPage',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('page_key', models.CharField(db_index=True, max_length=50, unique=True, verbose_name='页面标识')),
|
|
||||||
('name', models.CharField(max_length=100, verbose_name='页面名称')),
|
|
||||||
('description', models.TextField(blank=True, verbose_name='描述')),
|
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='是否启用')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
('ignore_user_mute', models.BooleanField(default=False, verbose_name='忽略用户今日屏蔽')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '弹窗页面',
|
|
||||||
'verbose_name_plural': '弹窗页面',
|
|
||||||
'db_table': 'popup_page',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='PopupImage',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('image_url', models.CharField(max_length=500, verbose_name='图片相对路径')),
|
|
||||||
('text', models.TextField(blank=True, verbose_name='图片上方文字')),
|
|
||||||
('sort_order', models.PositiveIntegerField(default=0, verbose_name='展示顺序')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('popup_config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to='config.popupconfig')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '弹窗图片',
|
|
||||||
'verbose_name_plural': '弹窗图片',
|
|
||||||
'db_table': 'popup_image',
|
|
||||||
'ordering': ['sort_order', 'id'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='popupconfig',
|
|
||||||
name='page',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='popups', to='config.popuppage'),
|
|
||||||
),
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name='popupconfig',
|
|
||||||
unique_together={('page', 'popup_id')},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-05-18 02:42
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('users', '0012_usershenheguan'),
|
('config', '0010_club_miniapp_icon_pindao'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='usershenheguan',
|
model_name='clubminiappicon',
|
||||||
name='is_renzheng',
|
name='is_uploaded',
|
||||||
field=models.BooleanField(default=False, verbose_name='是否认证'),
|
field=models.BooleanField(default=False, verbose_name='是否已在后台上传'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-04-25 12:05
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('config', '0010_popupconfig_popuppage_popupimage_popupconfig_page_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='WithdrawConfig',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('mode', models.IntegerField(choices=[(1, '自动提现'), (2, '手动提现')], default=2, verbose_name='提现模式')),
|
|
||||||
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
|
||||||
('updated_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '提现模式配置',
|
|
||||||
'verbose_name_plural': '提现模式配置',
|
|
||||||
'db_table': 'withdraw_config',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
52
config/migrations/0012_miniapp_script.py
Normal file
52
config/migrations/0012_miniapp_script.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('config', '0011_clubminiappicon_is_uploaded'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MiniappScriptScene',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')),
|
||||||
|
('scene_key', models.CharField(db_index=True, max_length=64, verbose_name='场景键')),
|
||||||
|
('title', models.CharField(blank=True, default='', max_length=128, verbose_name='标题')),
|
||||||
|
('content', models.TextField(blank=True, default='', verbose_name='正文')),
|
||||||
|
('confirm_text', models.CharField(blank=True, default='', max_length=16, verbose_name='确认按钮')),
|
||||||
|
('cancel_text', models.CharField(blank=True, default='', max_length=16, verbose_name='取消按钮')),
|
||||||
|
('enabled', models.BooleanField(default=True, verbose_name='启用')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '小程序场景话术',
|
||||||
|
'verbose_name_plural': '小程序场景话术',
|
||||||
|
'db_table': 'miniapp_script_scene',
|
||||||
|
'unique_together': {('club_id', 'scene_key')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='MiniappScriptAutoReply',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('club_id', models.CharField(db_index=True, default='xq', max_length=16, verbose_name='俱乐部ID')),
|
||||||
|
('keyword', models.CharField(max_length=200, verbose_name='关键词')),
|
||||||
|
('reply_type', models.CharField(choices=[('text', '文本'), ('image', '图片')], default='text', max_length=16, verbose_name='回复类型')),
|
||||||
|
('reply_content', models.TextField(verbose_name='回复内容(文本或图片URL)')),
|
||||||
|
('sort_order', models.IntegerField(default=0, verbose_name='排序(大优先)')),
|
||||||
|
('enabled', models.BooleanField(default=True, verbose_name='启用')),
|
||||||
|
('CreateTime', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '客服自动回复规则',
|
||||||
|
'verbose_name_plural': '客服自动回复规则',
|
||||||
|
'db_table': 'miniapp_script_auto_reply',
|
||||||
|
'ordering': ['-sort_order', 'id'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-05-17 18:57
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('config', '0011_withdrawconfig'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='shangjiamoban',
|
|
||||||
name='chenghao_id',
|
|
||||||
field=models.IntegerField(blank=True, null=True, verbose_name='关联称号ID'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='shangjiamoban',
|
|
||||||
name='yongjin_jiage',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='佣金价格'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='shangjiamoban',
|
|
||||||
name='yongjin_qiangdan',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='是否开启佣金抢单'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-05-26 03:25
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('config', '0012_shangjiamoban_chenghao_id_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='shangjialianjie',
|
|
||||||
name='previous_dingdan_id',
|
|
||||||
field=models.CharField(blank=True, db_index=True, max_length=32, null=True, verbose_name='上一个订单ID'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='shangjialianjie',
|
|
||||||
name='previous_lianjie_id',
|
|
||||||
field=models.PositiveIntegerField(blank=True, db_index=True, null=True, verbose_name='上一个链接ID'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Generated by Django 4.2.27 on 2026-06-15 01:42
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('config', '0013_shangjialianjie_previous_dingdan_id_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='Club',
|
|
||||||
),
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='ClubConfig',
|
|
||||||
),
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='DailyDispatchStat',
|
|
||||||
),
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='OrderTypeMapping',
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='tixianquotadefault',
|
|
||||||
name='leixing',
|
|
||||||
field=models.IntegerField(choices=[(1, '打手'), (2, '管事'), (3, '组长'), (4, '考核官'), (5, '打手押金'), (6, '商家余额')], unique=True, verbose_name='用户类型'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
1003
config/models.py
1003
config/models.py
File diff suppressed because it is too large
Load Diff
126
config/tasks.py
126
config/tasks.py
@@ -1,85 +1,43 @@
|
|||||||
"""
|
"""
|
||||||
阿龙电竞 - 收支记录定时任务
|
收支记录定时任务:每日清零各俱乐部今日流水/今日支出。
|
||||||
处理收支记录表的每日清零
|
"""
|
||||||
固定使用ID=1的记录
|
from celery import shared_task
|
||||||
"""
|
from datetime import datetime
|
||||||
from celery import shared_task
|
import logging
|
||||||
from django.db import transaction
|
|
||||||
from django.utils import timezone
|
from jituan.services.szjilu_accounting import reset_all_daily_szjilu
|
||||||
from datetime import datetime
|
from utils.celery_utils import log_task_execution, rollback_on_failure
|
||||||
from decimal import Decimal
|
|
||||||
import logging
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# 导入模型
|
|
||||||
from config.models import Szjilu
|
@shared_task
|
||||||
from utils.celery_utils import log_task_execution, rollback_on_failure
|
@rollback_on_failure("收支记录每日清零")
|
||||||
|
def daily_sz_reset_task():
|
||||||
logger = logging.getLogger(__name__)
|
"""收支记录每日清零 - 每天凌晨0点5分执行。"""
|
||||||
|
try:
|
||||||
@shared_task
|
today = datetime.now().strftime('%Y-%m-%d')
|
||||||
@rollback_on_failure("收支记录每日清零")
|
logger.info(f"开始执行收支记录清零任务,日期: {today}")
|
||||||
def daily_sz_reset_task():
|
|
||||||
"""
|
updated = reset_all_daily_szjilu()
|
||||||
收支记录每日清零 - 每天凌晨0点5分执行
|
result_msg = f"收支记录清零完成 - 日期: {today}, 俱乐部记录数: {updated}"
|
||||||
清零收支记录表的今日相关字段,固定使用ID=1的记录
|
log_task_execution("收支记录每日清零", True, result_msg)
|
||||||
"""
|
|
||||||
try:
|
return {
|
||||||
with transaction.atomic():
|
"success": True,
|
||||||
# 获取今天的日期
|
"date": today,
|
||||||
today = datetime.now().strftime('%Y-%m-%d')
|
"clubs_updated": updated,
|
||||||
|
"message": result_msg,
|
||||||
logger.info(f"开始执行收支记录清零任务,日期: {today}")
|
}
|
||||||
|
|
||||||
# 查询ID=1的收支记录,如果不存在则创建
|
except Exception as e:
|
||||||
try:
|
error_msg = f"收支记录每日清零失败: {str(e)}"
|
||||||
szjilu = Szjilu.objects.get(id=1)
|
logger.error(error_msg)
|
||||||
logger.info(f"找到ID=1的收支记录")
|
log_task_execution("收支记录每日清零", False, error_msg)
|
||||||
except Szjilu.DoesNotExist:
|
|
||||||
# 创建ID=1的初始记录
|
return {
|
||||||
szjilu = Szjilu.objects.create(
|
"success": False,
|
||||||
id=1,
|
"error": str(e),
|
||||||
zongsy=Decimal('0.00'),
|
"date": datetime.now().strftime('%Y-%m-%d'),
|
||||||
zongls=Decimal('0.00'),
|
"message": error_msg,
|
||||||
zongzc=Decimal('0.00'),
|
|
||||||
jrzc=Decimal('0.00'),
|
|
||||||
jrls=Decimal('0.00')
|
|
||||||
)
|
|
||||||
logger.info(f"创建ID=1的收支记录")
|
|
||||||
|
|
||||||
# 记录清零前的值(用于日志)
|
|
||||||
old_jrzc = szjilu.jrzc
|
|
||||||
old_jrls = szjilu.jrls
|
|
||||||
|
|
||||||
# 清零今日字段
|
|
||||||
szjilu.jrzc = Decimal('0.00')
|
|
||||||
szjilu.jrls = Decimal('0.00')
|
|
||||||
szjilu.save(update_fields=['jrzc', 'jrls', 'update_time'])
|
|
||||||
|
|
||||||
logger.info(f"收支记录清零完成,今日支出原值: {old_jrzc},今日流水原值: {old_jrls}")
|
|
||||||
|
|
||||||
result_msg = f"收支记录清零完成 - 日期: {today}, 记录ID: {szjilu.id}"
|
|
||||||
|
|
||||||
log_task_execution("收支记录每日清零", True, result_msg)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"date": today,
|
|
||||||
"record_id": szjilu.id,
|
|
||||||
"jrzc_cleared": True,
|
|
||||||
"jrls_cleared": True,
|
|
||||||
"old_jrzc": str(old_jrzc),
|
|
||||||
"old_jrls": str(old_jrls),
|
|
||||||
"message": result_msg
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = f"收支记录每日清零失败: {str(e)}"
|
|
||||||
logger.error(error_msg)
|
|
||||||
log_task_execution("收支记录每日清零", False, error_msg)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"error": str(e),
|
|
||||||
"date": datetime.now().strftime('%Y-%m-%d'),
|
|
||||||
"message": error_msg
|
|
||||||
}
|
}
|
||||||
@@ -3,9 +3,9 @@ from django.urls import path
|
|||||||
from .views import ShangpinGonggaoView, AdminUpdateConfigView, AdminUploadImageView, AdminConfigQueryView, \
|
from .views import ShangpinGonggaoView, AdminUpdateConfigView, AdminUploadImageView, AdminConfigQueryView, \
|
||||||
DashouPaihangView,GetAdminZhanghaoView, GetXiugaiJiluView,ShangjiaTianjiaMobanView, ShangjiaMobanListView, \
|
DashouPaihangView,GetAdminZhanghaoView, GetXiugaiJiluView,ShangjiaTianjiaMobanView, ShangjiaMobanListView, \
|
||||||
ShangjiaGengxinMobanView, ShangjiaShanchuMobanView, ShangjiaGenerateLinkView,KehuGetDingdanLianjieView, \
|
ShangjiaGengxinMobanView, ShangjiaShanchuMobanView, ShangjiaGenerateLinkView,KehuGetDingdanLianjieView, \
|
||||||
KehuTianxieDingdanView,GuanZhuAListView, ZuzhangHaibaoView,GuanshiQRCodeView,\
|
KehuTianxieDingdanView, GuanZhuAListView, ZuzhangHaibaoView,GuanshiQRCodeView,\
|
||||||
GetDynamicConfigView, PopupConfigView, GetWithdrawModeView,CheckPhoneAuthView,\
|
GetDynamicConfigView, HaibaoPeizhiView, PopupConfigView, GetWithdrawModeView,CheckPhoneAuthView,\
|
||||||
ShangjiaLianjieListView
|
ShangjiaLianjieListView, HuashuHqView, HuashuMatchView
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -32,10 +32,13 @@ urlpatterns = [
|
|||||||
path('guanshiewm', GuanshiQRCodeView.as_view(), name='管事生成二维码'),
|
path('guanshiewm', GuanshiQRCodeView.as_view(), name='管事生成二维码'),
|
||||||
path('zuzhanghb', ZuzhangHaibaoView.as_view(), name='组长二维码'),
|
path('zuzhanghb', ZuzhangHaibaoView.as_view(), name='组长二维码'),
|
||||||
path('qdpzhq', GetDynamicConfigView.as_view(), name='小程序获取配置'),
|
path('qdpzhq', GetDynamicConfigView.as_view(), name='小程序获取配置'),
|
||||||
|
path('haibaopz', HaibaoPeizhiView.as_view(), name='推广海报配置'),
|
||||||
|
|
||||||
path('tanchuang', PopupConfigView.as_view(), name='微信小程序获取弹窗数据'),
|
path('tanchuang', PopupConfigView.as_view(), name='微信小程序获取弹窗数据'),
|
||||||
path('hqtxzsym', GetWithdrawModeView.as_view(), name='小程序获取提现方式'),
|
path('hqtxzsym', GetWithdrawModeView.as_view(), name='小程序获取提现方式'),
|
||||||
|
|
||||||
path('yhbdsjh', CheckPhoneAuthView.as_view(), name='判断是否需要手机号绑定'),
|
path('yhbdsjh', CheckPhoneAuthView.as_view(), name='判断是否需要手机号绑定'),
|
||||||
path('hqsjlslj', ShangjiaLianjieListView.as_view(), name='商家获取生成链接'),
|
path('hqsjlslj', ShangjiaLianjieListView.as_view(), name='商家获取生成链接'),
|
||||||
|
path('huashuhq', HuashuHqView.as_view(), name='小程序场景话术获取'),
|
||||||
|
path('huashu_match', HuashuMatchView.as_view(), name='客服关键词自动回复'),
|
||||||
]
|
]
|
||||||
|
|||||||
3318
config/views.py
3318
config/views.py
File diff suppressed because it is too large
Load Diff
74
config/views/__init__.py
Normal file
74
config/views/__init__.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"""
|
||||||
|
配置应用视图聚合模块(config/views/__init__.py)
|
||||||
|
|
||||||
|
本模块原为 config/views.py(3718 行单文件),已按功能组拆分到 config/views/ 子包。
|
||||||
|
本文件作为聚合 re-export 入口,重新导出所有视图类与公共符号。
|
||||||
|
外部引用(config/urls.py、backend/urls.py、merchant_ops/views_templates.py)保持不变。
|
||||||
|
|
||||||
|
功能组拆分:
|
||||||
|
- miniapp_config 小程序动态配置(动态配置/海报/弹窗/提现方式/手机号认证)
|
||||||
|
- admin_config 管理员配置(商品公告/配置查询/图片上传/群与利率更新)
|
||||||
|
- admin_stats 管理员统计/记录(打手排行/账号查询/修改记录)
|
||||||
|
- merchant_templates 商家订单模板
|
||||||
|
- merchant_links 商家链接/客户订单(含 IPUtils、OrderLinkThrottle)
|
||||||
|
- qrcode_poster 二维码/海报(关注/管事二维码/组长海报)
|
||||||
|
- scripts 话术配置(含辅助函数)
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# 各功能组视图
|
||||||
|
from .miniapp_config import (
|
||||||
|
GetDynamicConfigView, HaibaoPeizhiView, PopupConfigView,
|
||||||
|
GetWithdrawModeView, CheckPhoneAuthView,
|
||||||
|
)
|
||||||
|
from .admin_config import (
|
||||||
|
ShangpinGonggaoView, AdminConfigQueryView, AdminUploadImageView,
|
||||||
|
AdminUpdateConfigView,
|
||||||
|
)
|
||||||
|
from .admin_stats import (
|
||||||
|
DashouPaihangView, GetAdminZhanghaoView, GetXiugaiJiluView,
|
||||||
|
)
|
||||||
|
from .merchant_templates import (
|
||||||
|
ShangjiaMobanListView, ShangjiaTianjiaMobanView,
|
||||||
|
ShangjiaShanchuMobanView, ShangjiaGengxinMobanView,
|
||||||
|
)
|
||||||
|
from .merchant_links import (
|
||||||
|
ShangjiaGenerateLinkView, ShangjiaLianjieListView,
|
||||||
|
KehuGetDingdanLianjieView, KehuTianxieDingdanView,
|
||||||
|
IPUtils, OrderLinkThrottle,
|
||||||
|
)
|
||||||
|
from .qrcode_poster import (
|
||||||
|
GuanZhuAListView, GuanshiQRCodeView, ZuzhangHaibaoView,
|
||||||
|
)
|
||||||
|
from .scripts import (
|
||||||
|
HuashuHqView, HuashuMatchView,
|
||||||
|
MiniappScriptHoutaiListView, MiniappScriptHoutaiModifyView,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 模块级符号(保持向后兼容)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# miniapp_config
|
||||||
|
'GetDynamicConfigView', 'HaibaoPeizhiView', 'PopupConfigView',
|
||||||
|
'GetWithdrawModeView', 'CheckPhoneAuthView',
|
||||||
|
# admin_config
|
||||||
|
'ShangpinGonggaoView', 'AdminConfigQueryView', 'AdminUploadImageView',
|
||||||
|
'AdminUpdateConfigView',
|
||||||
|
# admin_stats
|
||||||
|
'DashouPaihangView', 'GetAdminZhanghaoView', 'GetXiugaiJiluView',
|
||||||
|
# merchant_templates
|
||||||
|
'ShangjiaMobanListView', 'ShangjiaTianjiaMobanView',
|
||||||
|
'ShangjiaShanchuMobanView', 'ShangjiaGengxinMobanView',
|
||||||
|
# merchant_links
|
||||||
|
'ShangjiaGenerateLinkView', 'ShangjiaLianjieListView',
|
||||||
|
'KehuGetDingdanLianjieView', 'KehuTianxieDingdanView',
|
||||||
|
'IPUtils', 'OrderLinkThrottle',
|
||||||
|
# qrcode_poster
|
||||||
|
'GuanZhuAListView', 'GuanshiQRCodeView', 'ZuzhangHaibaoView',
|
||||||
|
# scripts
|
||||||
|
'HuashuHqView', 'HuashuMatchView',
|
||||||
|
'MiniappScriptHoutaiListView', 'MiniappScriptHoutaiModifyView',
|
||||||
|
# 公共符号
|
||||||
|
'logger',
|
||||||
|
]
|
||||||
612
config/views/admin_config.py
Normal file
612
config/views/admin_config.py
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import secrets
|
||||||
|
import hashlib
|
||||||
|
import threading
|
||||||
|
import urllib.parse
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import transaction, connection
|
||||||
|
from django.db.models import Q, F, Max, Prefetch
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status, permissions
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
|
||||||
|
|
||||||
|
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
|
||||||
|
from utils.weixin_broadcast import WeixinBroadcastSender
|
||||||
|
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||||||
|
# from utils.ip_security import *
|
||||||
|
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||||
|
|
||||||
|
from backend.utils import update_shangjia_daily
|
||||||
|
from utils.chat_utils import subscribe_merchant_link_chat
|
||||||
|
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||||
|
|
||||||
|
from ..models import (
|
||||||
|
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||||||
|
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||||||
|
MiniappScriptScene, MiniappScriptAutoReply,
|
||||||
|
)
|
||||||
|
|
||||||
|
from users.models import (
|
||||||
|
AdminProfile, UserDashou, UserBoss,
|
||||||
|
UserShangjia, UserGuanshi, UserZuzhang
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
from orders.models import CommissionRate, Order, MerchantOrderExt
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
|
||||||
|
from rank.models import Chenghao, DingdanBiaoqian
|
||||||
|
|
||||||
|
from products.models import ShangpinLeixing, Huiyuangoumai
|
||||||
|
|
||||||
|
from ..serializers import PopupConfigSerializer
|
||||||
|
import traceback
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ShangpinGonggaoView(APIView):
|
||||||
|
"""
|
||||||
|
商品公告和轮播图接口
|
||||||
|
获取类型为1的公告和轮播图
|
||||||
|
"""
|
||||||
|
throttle_classes = [AnonRateThrottle]
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
authentication_classes = []
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
处理POST请求,返回公告和轮播图数据
|
||||||
|
|
||||||
|
请求: POST /api/peizhi/shangpingonggao/
|
||||||
|
返回: {
|
||||||
|
"shangpingonggao": "公告内容字符串",
|
||||||
|
"shangpinlunbo": ["轮播图URL1", "轮播图URL2"]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
logger.info("收到商品公告和轮播图请求")
|
||||||
|
from jituan.services.display_config import get_gonggao_content, get_lunbo_urls, normalize_page_key
|
||||||
|
|
||||||
|
page_key = normalize_page_key(request.data.get('page_key'), image_type=1)
|
||||||
|
gonggao_content = get_gonggao_content(request, notice_type=1, page_key=page_key)
|
||||||
|
lunbo_urls = get_lunbo_urls(request, page_key=page_key, image_type=1)
|
||||||
|
response_data = {
|
||||||
|
"shangpingonggao": gonggao_content,
|
||||||
|
"shangpinlunbo": lunbo_urls,
|
||||||
|
"page_key": page_key,
|
||||||
|
}
|
||||||
|
logger.info(f"返回数据:公告长度{len(gonggao_content)},轮播图数量{len(lunbo_urls)} page_key={page_key}")
|
||||||
|
# 4. 返回响应
|
||||||
|
return Response(response_data, status=status.HTTP_200_OK)
|
||||||
|
except Exception as e:
|
||||||
|
# 记录异常
|
||||||
|
logger.error(f"获取商品公告和轮播图时发生错误:{str(e)}")
|
||||||
|
# 返回错误响应
|
||||||
|
error_data = {
|
||||||
|
"error": "服务器内部错误",
|
||||||
|
"detail": "获取数据失败,请稍后重试"
|
||||||
|
}
|
||||||
|
return Response(error_data, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AdminConfigQueryView(APIView):
|
||||||
|
"""
|
||||||
|
管理员获取系统配置接口
|
||||||
|
URL: /peizhi/adpzhq
|
||||||
|
方法: POST
|
||||||
|
权限: JWT Token认证 + 管理员权限
|
||||||
|
参数: zhanghao (管理员账号)
|
||||||
|
"""
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
# 获取前端传递的账号
|
||||||
|
zhanghao = request.data.get('zhanghao')
|
||||||
|
if not zhanghao:
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '账号不能为空',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 获取当前用户(User实例)
|
||||||
|
user_main = request.user
|
||||||
|
|
||||||
|
# 验证账号:比较user_main的phone与前端传递的zhanghao
|
||||||
|
if user_main.Phone != zhanghao:
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '账号不匹配',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 验证用户类型是否为管理员
|
||||||
|
if user_main.UserType != 'admin':
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '无权限',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 验证管理员扩展表是否存在
|
||||||
|
try:
|
||||||
|
admin_profile = user_main.AdminProfile
|
||||||
|
except AdminProfile.DoesNotExist:
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '无权限',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 查询图片配置
|
||||||
|
rule_image = Tupianpeizhi.query.filter(ImageType=1).first()
|
||||||
|
default_avatar = Tupianpeizhi.query.filter(ImageType=2).first()
|
||||||
|
|
||||||
|
# 查询群配置
|
||||||
|
qun_configs = Qunpeizhi.query.all()
|
||||||
|
qq_groups = []
|
||||||
|
for config in qun_configs:
|
||||||
|
qq_groups.append({
|
||||||
|
'peizhiid': config.GroupType,
|
||||||
|
'neirong': config.GroupContent if config.GroupContent else '',
|
||||||
|
'qunid': config.GroupID if config.GroupID else '',
|
||||||
|
'jieshao': config.Description if config.Description else ''
|
||||||
|
})
|
||||||
|
|
||||||
|
# 查询利率配置
|
||||||
|
platform_rate_obj = CommissionRate.query.filter(Platform='1').first()
|
||||||
|
shop_rate_obj = CommissionRate.query.filter(Platform='3').first()
|
||||||
|
# 🔴【新增】打手提现抽成利率(字符5)
|
||||||
|
dashou_withdraw_obj = CommissionRate.query.filter(Platform='5').first()
|
||||||
|
# 🔴【新增】管事提现抽成利率(字符6)
|
||||||
|
guanshi_withdraw_obj = CommissionRate.query.filter(Platform='6').first()
|
||||||
|
|
||||||
|
# 构建返回数据
|
||||||
|
data = {
|
||||||
|
'images': {
|
||||||
|
'rule_image': rule_image.ImageURL if rule_image and rule_image.ImageURL else '',
|
||||||
|
'default_avatar': default_avatar.ImageURL if default_avatar and default_avatar.ImageURL else ''
|
||||||
|
},
|
||||||
|
'qq_groups': qq_groups,
|
||||||
|
'rates': {
|
||||||
|
'platform_rate': float(platform_rate_obj.Rate) if platform_rate_obj and platform_rate_obj.Rate else 0.0,
|
||||||
|
'shop_rate': float(shop_rate_obj.Rate) if shop_rate_obj and shop_rate_obj.Rate else 0.0,
|
||||||
|
# 🔴【新增】打手提现抽成利率
|
||||||
|
'dashou_withdraw_rate': float(
|
||||||
|
dashou_withdraw_obj.Rate) if dashou_withdraw_obj and dashou_withdraw_obj.Rate else 0.0,
|
||||||
|
# 🔴【新增】管事提现抽成利率
|
||||||
|
'guanshi_withdraw_rate': float(
|
||||||
|
guanshi_withdraw_obj.Rate) if guanshi_withdraw_obj and guanshi_withdraw_obj.Rate else 0.0
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'success',
|
||||||
|
'data': data
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return Response({
|
||||||
|
'code': 99,
|
||||||
|
'msg': f'系统错误: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminUploadImageView(APIView):
|
||||||
|
"""
|
||||||
|
管理员上传配置图片接口
|
||||||
|
URL: /peizhi/adxgtp
|
||||||
|
方法: POST
|
||||||
|
权限: JWT Token认证 + 管理员权限
|
||||||
|
参数: zhanghao, leixing (1:打手规则图片, 2:默认头像图片), image (文件)
|
||||||
|
"""
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
parser_classes = (MultiPartParser, FormParser)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
zhanghao = request.data.get('zhanghao')
|
||||||
|
leixing = request.data.get('leixing')
|
||||||
|
image_file = request.FILES.get('image')
|
||||||
|
|
||||||
|
if not zhanghao:
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '账号不能为空',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if not leixing:
|
||||||
|
return Response({
|
||||||
|
'code': 2,
|
||||||
|
'msg': '图片类型不能为空',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if not image_file:
|
||||||
|
return Response({
|
||||||
|
'code': 3,
|
||||||
|
'msg': '请选择图片文件',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 验证用户权限
|
||||||
|
user_main = request.user
|
||||||
|
if user_main.Phone != zhanghao:
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '账号不匹配',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
if user_main.UserType != 'admin':
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '无权限',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
try:
|
||||||
|
admin_profile = user_main.AdminProfile
|
||||||
|
except AdminProfile.DoesNotExist:
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '无权限',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 验证图片类型
|
||||||
|
if leixing not in ['1', '2']:
|
||||||
|
return Response({
|
||||||
|
'code': 4,
|
||||||
|
'msg': '图片类型错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 验证图片文件
|
||||||
|
is_valid, error_msg = validate_image(image_file)
|
||||||
|
if not is_valid:
|
||||||
|
return Response({
|
||||||
|
'code': 5,
|
||||||
|
'msg': f'图片验证失败: {error_msg}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 根据类型确定固定相对URL和文件名
|
||||||
|
if leixing == '1':
|
||||||
|
# 打手规则图片
|
||||||
|
fixed_relative_url = 'a_long/dashouguize.jpg'
|
||||||
|
oss_file_name = 'dashouguize.jpg'
|
||||||
|
else:
|
||||||
|
# 默认头像图片
|
||||||
|
fixed_relative_url = 'a_long/morentouxiang.jpg'
|
||||||
|
oss_file_name = 'morentouxiang.jpg'
|
||||||
|
|
||||||
|
# 构建OSS文件路径
|
||||||
|
oss_file_path = fixed_relative_url # a_long/dashouguize.jpg 或 a_long/morentouxiang.jpg
|
||||||
|
|
||||||
|
# 上传到OSS(覆盖原有文件)
|
||||||
|
image_file.seek(0) # 重置文件指针
|
||||||
|
full_url = upload_to_oss(image_file, oss_file_path)
|
||||||
|
|
||||||
|
if not full_url:
|
||||||
|
return Response({
|
||||||
|
'code': 6,
|
||||||
|
'msg': '图片上传到存储桶失败',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
# 更新数据库
|
||||||
|
with transaction.atomic():
|
||||||
|
# 获取或创建图片配置记录
|
||||||
|
tupian_peizhi, created = Tupianpeizhi.query.get_or_create(ImageType=int(leixing))
|
||||||
|
tupian_peizhi.ImageURL = fixed_relative_url
|
||||||
|
tupian_peizhi.save()
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': '图片上传成功',
|
||||||
|
'data': {
|
||||||
|
'image_url': fixed_relative_url
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return Response({
|
||||||
|
'code': 99,
|
||||||
|
'msg': f'系统错误: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminUpdateConfigView(APIView):
|
||||||
|
"""
|
||||||
|
管理员更新系统配置接口(QQ群、利率)- 已修复
|
||||||
|
URL: /peizhi/adupdate
|
||||||
|
方法: POST
|
||||||
|
权限: JWT Token认证 + 管理员权限
|
||||||
|
参数:
|
||||||
|
- zhanghao: 管理员账号
|
||||||
|
- qq_groups: [{"peizhiid": 1, "neirong": "", "qunid": "", "jieshao": ""}, ...] (可选)
|
||||||
|
- rates: {"platform_rate": 0.1, "shop_rate": 0.2} (可选)
|
||||||
|
"""
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
zhanghao = request.data.get('zhanghao')
|
||||||
|
if not zhanghao:
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '账号不能为空',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 验证用户权限
|
||||||
|
user_main = request.user
|
||||||
|
if user_main.Phone != zhanghao:
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '账号不匹配',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
if user_main.UserType != 'admin':
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '无权限',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
try:
|
||||||
|
admin_profile = user_main.AdminProfile
|
||||||
|
except AdminProfile.DoesNotExist:
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '无权限',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
response_data = {}
|
||||||
|
|
||||||
|
# 🔴 修复:更新QQ群配置 - 确保根据peizhiid正确更新对应的记录
|
||||||
|
qq_groups = request.data.get('qq_groups')
|
||||||
|
if qq_groups and isinstance(qq_groups, list):
|
||||||
|
updated_groups = []
|
||||||
|
for group_data in qq_groups:
|
||||||
|
peizhiid = group_data.get('peizhiid')
|
||||||
|
if not peizhiid:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 🔴 修复:使用filter()确保查询到正确的记录
|
||||||
|
qun_configs = Qunpeizhi.query.filter(GroupType=peizhiid)
|
||||||
|
|
||||||
|
if qun_configs.exists():
|
||||||
|
# 如果存在多个相同peizhiid的记录,取第一个(应该只有一个)
|
||||||
|
qun_config = qun_configs.first()
|
||||||
|
|
||||||
|
# 更新字段
|
||||||
|
neirong = group_data.get('neirong')
|
||||||
|
qunid = group_data.get('qunid')
|
||||||
|
jieshao = group_data.get('jieshao')
|
||||||
|
|
||||||
|
if neirong is not None:
|
||||||
|
qun_config.GroupContent = neirong or ''
|
||||||
|
if qunid is not None:
|
||||||
|
qun_config.GroupID = qunid or ''
|
||||||
|
if jieshao is not None:
|
||||||
|
qun_config.Description = jieshao or ''
|
||||||
|
|
||||||
|
qun_config.save()
|
||||||
|
else:
|
||||||
|
# 如果不存在,创建新的(使用提供的peizhiid)
|
||||||
|
qun_config = Qunpeizhi.query.create(
|
||||||
|
GroupType=peizhiid,
|
||||||
|
GroupContent=group_data.get('neirong', '') or '',
|
||||||
|
GroupID=group_data.get('qunid', '') or '',
|
||||||
|
Description=group_data.get('jieshao', '') or ''
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_groups.append({
|
||||||
|
'peizhiid': qun_config.GroupType,
|
||||||
|
'neirong': qun_config.GroupContent,
|
||||||
|
'qunid': qun_config.GroupID,
|
||||||
|
'jieshao': qun_config.Description
|
||||||
|
})
|
||||||
|
|
||||||
|
if updated_groups:
|
||||||
|
response_data['qq_groups'] = updated_groups
|
||||||
|
|
||||||
|
# 🔴 修复:更新利率配置 - 确保更新现有记录而不是创建新记录
|
||||||
|
rates = request.data.get('rates')
|
||||||
|
if rates and isinstance(rates, dict):
|
||||||
|
updated_rates = {}
|
||||||
|
|
||||||
|
# 平台发单利率(类型1,fadanpingtai='1')
|
||||||
|
platform_rate = rates.get('platform_rate')
|
||||||
|
if platform_rate is not None:
|
||||||
|
try:
|
||||||
|
platform_rate_float = float(platform_rate)
|
||||||
|
if 0 <= platform_rate_float <= 1:
|
||||||
|
# 🔴 修复:查询现有记录,如果不存在则创建
|
||||||
|
platform_rate_obj = CommissionRate.query.filter(Platform='1').first()
|
||||||
|
|
||||||
|
if platform_rate_obj:
|
||||||
|
# 更新现有记录
|
||||||
|
platform_rate_obj.Rate = platform_rate_float
|
||||||
|
platform_rate_obj.save()
|
||||||
|
else:
|
||||||
|
# 创建新记录
|
||||||
|
platform_rate_obj = CommissionRate.query.create(
|
||||||
|
Rate=platform_rate_float,
|
||||||
|
Platform='1'
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_rates['platform_rate'] = platform_rate_float
|
||||||
|
else:
|
||||||
|
return Response({
|
||||||
|
'code': 3,
|
||||||
|
'msg': '平台发单利率必须在0-1之间',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except ValueError:
|
||||||
|
return Response({
|
||||||
|
'code': 4,
|
||||||
|
'msg': '平台发单利率格式错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 商家发单利率(类型2,fadanpingtai='2')
|
||||||
|
shop_rate = rates.get('shop_rate')
|
||||||
|
if shop_rate is not None:
|
||||||
|
try:
|
||||||
|
shop_rate_float = float(shop_rate)
|
||||||
|
if 0 <= shop_rate_float <= 1:
|
||||||
|
# 🔴 修复:查询现有记录,如果不存在则创建
|
||||||
|
shop_rate_obj = CommissionRate.query.filter(Platform='3').first()
|
||||||
|
|
||||||
|
if shop_rate_obj:
|
||||||
|
# 更新现有记录
|
||||||
|
shop_rate_obj.Rate = shop_rate_float
|
||||||
|
shop_rate_obj.save()
|
||||||
|
else:
|
||||||
|
# 创建新记录
|
||||||
|
shop_rate_obj = CommissionRate.query.create(
|
||||||
|
Rate=shop_rate_float,
|
||||||
|
Platform='3'
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_rates['shop_rate'] = shop_rate_float
|
||||||
|
else:
|
||||||
|
return Response({
|
||||||
|
'code': 5,
|
||||||
|
'msg': '商家发单利率必须在0-1之间',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except ValueError:
|
||||||
|
return Response({
|
||||||
|
'code': 6,
|
||||||
|
'msg': '商家发单利率格式错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 🔴【新增】打手提现抽成利率(类型5,fadanpingtai='5')
|
||||||
|
dashou_withdraw_rate = rates.get('dashou_withdraw_rate')
|
||||||
|
if dashou_withdraw_rate is not None:
|
||||||
|
try:
|
||||||
|
dashou_withdraw_rate_float = float(dashou_withdraw_rate)
|
||||||
|
if 0 <= dashou_withdraw_rate_float <= 1:
|
||||||
|
# 查询现有记录,如果不存在则创建
|
||||||
|
dashou_withdraw_obj = CommissionRate.query.filter(Platform='5').first()
|
||||||
|
|
||||||
|
if dashou_withdraw_obj:
|
||||||
|
# 更新现有记录
|
||||||
|
dashou_withdraw_obj.Rate = dashou_withdraw_rate_float
|
||||||
|
dashou_withdraw_obj.save()
|
||||||
|
else:
|
||||||
|
# 创建新记录
|
||||||
|
dashou_withdraw_obj = CommissionRate.query.create(
|
||||||
|
Rate=dashou_withdraw_rate_float,
|
||||||
|
Platform='5'
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_rates['dashou_withdraw_rate'] = dashou_withdraw_rate_float
|
||||||
|
else:
|
||||||
|
return Response({
|
||||||
|
'code': 7, # 注意:这里需要调整错误码,不要重复
|
||||||
|
'msg': '打手提现抽成利率必须在0-1之间',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except ValueError:
|
||||||
|
return Response({
|
||||||
|
'code': 8,
|
||||||
|
'msg': '打手提现抽成利率格式错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 🔴【新增】管事提现抽成利率(类型6,fadanpingtai='6')
|
||||||
|
guanshi_withdraw_rate = rates.get('guanshi_withdraw_rate')
|
||||||
|
if guanshi_withdraw_rate is not None:
|
||||||
|
try:
|
||||||
|
guanshi_withdraw_rate_float = float(guanshi_withdraw_rate)
|
||||||
|
if 0 <= guanshi_withdraw_rate_float <= 1:
|
||||||
|
# 查询现有记录,如果不存在则创建
|
||||||
|
guanshi_withdraw_obj = CommissionRate.query.filter(Platform='6').first()
|
||||||
|
|
||||||
|
if guanshi_withdraw_obj:
|
||||||
|
# 更新现有记录
|
||||||
|
guanshi_withdraw_obj.Rate = guanshi_withdraw_rate_float
|
||||||
|
guanshi_withdraw_obj.save()
|
||||||
|
else:
|
||||||
|
# 创建新记录
|
||||||
|
guanshi_withdraw_obj = CommissionRate.query.create(
|
||||||
|
Rate=guanshi_withdraw_rate_float,
|
||||||
|
Platform='6'
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_rates['guanshi_withdraw_rate'] = guanshi_withdraw_rate_float
|
||||||
|
else:
|
||||||
|
return Response({
|
||||||
|
'code': 9,
|
||||||
|
'msg': '管事提现抽成利率必须在0-1之间',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
except ValueError:
|
||||||
|
return Response({
|
||||||
|
'code': 10,
|
||||||
|
'msg': '管事提现抽成利率格式错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if updated_rates:
|
||||||
|
response_data['rates'] = updated_rates
|
||||||
|
|
||||||
|
# 如果没有任何更新
|
||||||
|
if not response_data:
|
||||||
|
return Response({
|
||||||
|
'code': 7,
|
||||||
|
'msg': '没有需要更新的数据',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': '更新成功',
|
||||||
|
'data': response_data
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return Response({
|
||||||
|
'code': 99,
|
||||||
|
'msg': f'系统错误: {str(e)}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
718
config/views/admin_stats.py
Normal file
718
config/views/admin_stats.py
Normal file
@@ -0,0 +1,718 @@
|
|||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import secrets
|
||||||
|
import hashlib
|
||||||
|
import threading
|
||||||
|
import urllib.parse
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import transaction, connection
|
||||||
|
from django.db.models import Q, F, Max, Prefetch
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status, permissions
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
|
||||||
|
|
||||||
|
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
|
||||||
|
from utils.weixin_broadcast import WeixinBroadcastSender
|
||||||
|
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||||||
|
# from utils.ip_security import *
|
||||||
|
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||||
|
|
||||||
|
from backend.utils import update_shangjia_daily
|
||||||
|
from utils.chat_utils import subscribe_merchant_link_chat
|
||||||
|
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||||
|
|
||||||
|
from ..models import (
|
||||||
|
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||||||
|
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||||||
|
MiniappScriptScene, MiniappScriptAutoReply,
|
||||||
|
)
|
||||||
|
|
||||||
|
from users.models import (
|
||||||
|
AdminProfile, UserDashou, UserBoss,
|
||||||
|
UserShangjia, UserGuanshi, UserZuzhang
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
from orders.models import CommissionRate, Order, MerchantOrderExt
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
|
||||||
|
from rank.models import Chenghao, DingdanBiaoqian
|
||||||
|
|
||||||
|
from products.models import ShangpinLeixing, Huiyuangoumai
|
||||||
|
|
||||||
|
from ..serializers import PopupConfigSerializer
|
||||||
|
import traceback
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class DashouPaihangView(APIView):
|
||||||
|
"""
|
||||||
|
排行榜接口
|
||||||
|
路径:/peizhi/dsph
|
||||||
|
方法:POST
|
||||||
|
认证:JWT Token认证
|
||||||
|
|
||||||
|
请求参数:
|
||||||
|
- leixing: 1=今日收入,2=今月收入
|
||||||
|
- yonghu_leixing: 1=打手,2=管事,3=商家
|
||||||
|
|
||||||
|
返回数据:
|
||||||
|
- code: 0=成功,其他=失败
|
||||||
|
- msg: 提示信息
|
||||||
|
- data: {
|
||||||
|
xianshi: True/False, # 是否展示真实数据
|
||||||
|
paihang_list: [ # 排行榜列表
|
||||||
|
{
|
||||||
|
uid: '用户ID',
|
||||||
|
touxiang: '头像相对URL',
|
||||||
|
nicheng: '用户昵称',
|
||||||
|
jine: 收入金额,
|
||||||
|
mingci: 排名
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =============== 可配置参数 ===============
|
||||||
|
# 每次查询返回的最大记录数(可修改)
|
||||||
|
ZUI_DAO_CHA_XUN_SHU_LIANG = 50
|
||||||
|
|
||||||
|
# 最少需要多少人才能展示(可修改)
|
||||||
|
ZUI_SHAO_REN_SHU_YAO_QIU = 1
|
||||||
|
|
||||||
|
# 第一名最低金额要求(单位:元)(可修改)
|
||||||
|
DI_YI_MING_ZUI_DI_JIN_E = Decimal('10.00')
|
||||||
|
|
||||||
|
# 管事第一名最低数量要求(可修改)
|
||||||
|
DI_YI_MING_ZUI_DI_SHU_LIANG = 1
|
||||||
|
# =============== 可配置参数结束 ===============
|
||||||
|
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
# 获取请求参数 - 修复参数类型问题
|
||||||
|
leixing_str = request.data.get('leixing', '1')
|
||||||
|
yonghu_leixing_str = request.data.get('yonghu_leixing', '1')
|
||||||
|
|
||||||
|
# 转换为整数
|
||||||
|
try:
|
||||||
|
leixing = int(leixing_str)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
leixing = 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
yonghu_leixing = int(yonghu_leixing_str)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
yonghu_leixing = 1
|
||||||
|
|
||||||
|
# 参数验证
|
||||||
|
if leixing not in [1, 2]:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'msg': '参数错误:leixing必须为1(今日)或2(今月)',
|
||||||
|
'data': {'xianshi': False}
|
||||||
|
})
|
||||||
|
|
||||||
|
if yonghu_leixing not in [1, 2, 3]:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'msg': '参数错误:yonghu_leixing必须为1(打手)、2(管事)或3(商家)',
|
||||||
|
'data': {'xianshi': False}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 根据用户类型调用不同的查询方法
|
||||||
|
if yonghu_leixing == 1:
|
||||||
|
# 打手排行榜
|
||||||
|
paihang_list, xianshi_flag = self.chaxun_dashou_paihang(leixing)
|
||||||
|
elif yonghu_leixing == 2:
|
||||||
|
# 管事排行榜
|
||||||
|
paihang_list, xianshi_flag = self.chaxun_guanshi_paihang(leixing)
|
||||||
|
else:
|
||||||
|
# 商家排行榜
|
||||||
|
paihang_list, xianshi_flag = self.chaxun_shangjia_paihang(leixing)
|
||||||
|
|
||||||
|
# 返回结果
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': '获取成功',
|
||||||
|
'data': {
|
||||||
|
'xianshi': xianshi_flag,
|
||||||
|
'paihang_list': paihang_list
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"排行榜查询失败: {str(e)}", exc_info=True)
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'msg': '服务器内部错误',
|
||||||
|
'data': {'xianshi': False}
|
||||||
|
})
|
||||||
|
|
||||||
|
def chaxun_dashou_paihang(self, leixing):
|
||||||
|
"""
|
||||||
|
查询打手排行榜
|
||||||
|
:param leixing: 1=今日收入,2=今月收入
|
||||||
|
:return: (排行榜列表, 是否展示标志)
|
||||||
|
"""
|
||||||
|
# 确定查询的字段
|
||||||
|
if leixing == 1:
|
||||||
|
# 今日收入
|
||||||
|
shouyi_ziduan = 'jinrishouyi'
|
||||||
|
else:
|
||||||
|
# 今月收入
|
||||||
|
shouyi_ziduan = 'jinyueshouyi'
|
||||||
|
|
||||||
|
# 查询条件:收入大于0,打手账号状态正常(1=正常)
|
||||||
|
# 注意:打手表中的zhanghaozhuangtai字段
|
||||||
|
chaxun_tiaojian = Q(**{f'{shouyi_ziduan}__gt': 0}) & Q(zhanghaozhuangtai=1)
|
||||||
|
|
||||||
|
# 查询打手扩展表,并关联用户主表
|
||||||
|
dashou_list = UserDashou.query.select_related('user').filter(
|
||||||
|
chaxun_tiaojian
|
||||||
|
).order_by(f'-{shouyi_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
|
||||||
|
|
||||||
|
# 检查是否满足展示条件
|
||||||
|
if len(dashou_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
|
||||||
|
# 人数不足,不展示
|
||||||
|
return [], False
|
||||||
|
|
||||||
|
# 检查第一名金额是否达到最低要求
|
||||||
|
if leixing == 1:
|
||||||
|
di_yi_ming_jine = dashou_list[0].jinrishouyi
|
||||||
|
else:
|
||||||
|
di_yi_ming_jine = dashou_list[0].jinyueshouyi
|
||||||
|
|
||||||
|
if di_yi_ming_jine < self.DI_YI_MING_ZUI_DI_JIN_E:
|
||||||
|
# 第一名金额不足,不展示
|
||||||
|
return [], False
|
||||||
|
|
||||||
|
# 构建返回数据
|
||||||
|
paihang_list = []
|
||||||
|
for index, dashou in enumerate(dashou_list):
|
||||||
|
# 获取用户主表信息
|
||||||
|
user_main = dashou.user
|
||||||
|
|
||||||
|
# 打手的昵称直接使用打手扩展表的nicheng字段
|
||||||
|
dashou_nicheng = dashou.nicheng or '未设置昵称'
|
||||||
|
|
||||||
|
# 获取头像(相对URL)
|
||||||
|
touxiang_url = user_main.Avatar or ''
|
||||||
|
|
||||||
|
# 获取收入金额
|
||||||
|
if leixing == 1:
|
||||||
|
shouyi_jine = dashou.jinrishouyi
|
||||||
|
else:
|
||||||
|
shouyi_jine = dashou.jinyueshouyi
|
||||||
|
|
||||||
|
paihang_list.append({
|
||||||
|
'uid': user_main.UserUID,
|
||||||
|
'touxiang': touxiang_url,
|
||||||
|
'nicheng': dashou_nicheng,
|
||||||
|
'jine': float(shouyi_jine), # 转换为float方便前端处理
|
||||||
|
'mingci': index + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
return paihang_list, True
|
||||||
|
|
||||||
|
def chaxun_guanshi_paihang(self, leixing):
|
||||||
|
"""
|
||||||
|
查询管事排行榜
|
||||||
|
:param leixing: 1=今日,2=今月
|
||||||
|
:return: (排行榜列表, 是否展示标志)
|
||||||
|
"""
|
||||||
|
# 确定查询的字段
|
||||||
|
if leixing == 1:
|
||||||
|
# 今日充值打手数量
|
||||||
|
chaxun_ziduan = 'jinrichongzhi'
|
||||||
|
else:
|
||||||
|
# 今月充值打手数量
|
||||||
|
chaxun_ziduan = 'jinyuechongzhi'
|
||||||
|
|
||||||
|
# 查询条件:数量大于0,管事状态正常(状态1为正常)
|
||||||
|
# 注意:管手表中的zhuangtai字段
|
||||||
|
chaxun_tiaojian = Q(**{f'{chaxun_ziduan}__gt': 0}) & Q(zhuangtai=1)
|
||||||
|
|
||||||
|
# 查询管事扩展表,并关联用户主表
|
||||||
|
guanshi_list = UserGuanshi.query.select_related('user').filter(
|
||||||
|
chaxun_tiaojian
|
||||||
|
).order_by(f'-{chaxun_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
|
||||||
|
|
||||||
|
# 检查是否满足展示条件
|
||||||
|
if len(guanshi_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
|
||||||
|
# 人数不足,不展示
|
||||||
|
return [], False
|
||||||
|
|
||||||
|
# 检查第一名数量是否达到最低要求
|
||||||
|
if leixing == 1:
|
||||||
|
di_yi_ming_shuliang = guanshi_list[0].jinrichongzhi
|
||||||
|
else:
|
||||||
|
di_yi_ming_shuliang = guanshi_list[0].jinyuechongzhi
|
||||||
|
|
||||||
|
if di_yi_ming_shuliang < self.DI_YI_MING_ZUI_DI_SHU_LIANG:
|
||||||
|
# 第一名数量不足,不展示
|
||||||
|
return [], False
|
||||||
|
|
||||||
|
# 构建返回数据
|
||||||
|
paihang_list = []
|
||||||
|
for index, guanshi in enumerate(guanshi_list):
|
||||||
|
# 获取用户主表信息
|
||||||
|
user_main = guanshi.user
|
||||||
|
|
||||||
|
# 管事的昵称需要查询老板扩展表
|
||||||
|
boss_profile = UserBoss.query.filter(user=user_main).first()
|
||||||
|
guanshi_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
|
||||||
|
|
||||||
|
# 获取头像(相对URL)
|
||||||
|
touxiang_url = user_main.Avatar or ''
|
||||||
|
|
||||||
|
# 获取数量
|
||||||
|
if leixing == 1:
|
||||||
|
chongzhi_shuliang = guanshi.jinrichongzhi
|
||||||
|
else:
|
||||||
|
chongzhi_shuliang = guanshi.jinyuechongzhi
|
||||||
|
|
||||||
|
paihang_list.append({
|
||||||
|
'uid': user_main.UserUID,
|
||||||
|
'touxiang': touxiang_url,
|
||||||
|
'nicheng': guanshi_nicheng,
|
||||||
|
'jine': float(chongzhi_shuliang), # 这里jine字段实际存储的是数量
|
||||||
|
'mingci': index + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
return paihang_list, True
|
||||||
|
|
||||||
|
def chaxun_shangjia_paihang(self, leixing):
|
||||||
|
"""
|
||||||
|
查询商家排行榜
|
||||||
|
:param leixing: 1=今日流水,2=今月流水
|
||||||
|
:return: (排行榜列表, 是否展示标志)
|
||||||
|
"""
|
||||||
|
# 确定查询的字段
|
||||||
|
if leixing == 1:
|
||||||
|
# 今日流水
|
||||||
|
liushui_ziduan = 'jinriliushui'
|
||||||
|
else:
|
||||||
|
# 今月流水
|
||||||
|
liushui_ziduan = 'jinyueliushui'
|
||||||
|
|
||||||
|
# 查询条件:流水大于0,商家状态正常(状态1为正常)
|
||||||
|
# 注意:商家表中的zhuangtai字段
|
||||||
|
chaxun_tiaojian = Q(**{f'{liushui_ziduan}__gt': 0}) & Q(zhuangtai=1)
|
||||||
|
|
||||||
|
# 查询商家扩展表,并关联用户主表
|
||||||
|
shangjia_list = UserShangjia.query.select_related('user').filter(
|
||||||
|
chaxun_tiaojian
|
||||||
|
).order_by(f'-{liushui_ziduan}')[:self.ZUI_DAO_CHA_XUN_SHU_LIANG]
|
||||||
|
|
||||||
|
# 检查是否满足展示条件
|
||||||
|
if len(shangjia_list) < self.ZUI_SHAO_REN_SHU_YAO_QIU:
|
||||||
|
# 人数不足,不展示
|
||||||
|
return [], False
|
||||||
|
|
||||||
|
# 检查第一名金额是否达到最低要求
|
||||||
|
if leixing == 1:
|
||||||
|
di_yi_ming_liushui = shangjia_list[0].jinriliushui
|
||||||
|
else:
|
||||||
|
di_yi_ming_liushui = shangjia_list[0].jinyueliushui
|
||||||
|
|
||||||
|
if di_yi_ming_liushui < self.DI_YI_MING_ZUI_DI_JIN_E:
|
||||||
|
# 第一名流水不足,不展示
|
||||||
|
return [], False
|
||||||
|
|
||||||
|
# 构建返回数据
|
||||||
|
paihang_list = []
|
||||||
|
for index, shangjia in enumerate(shangjia_list):
|
||||||
|
# 获取用户主表信息
|
||||||
|
user_main = shangjia.user
|
||||||
|
|
||||||
|
# 商家的昵称需要查询老板扩展表
|
||||||
|
boss_profile = UserBoss.query.filter(user=user_main).first()
|
||||||
|
shangjia_nicheng = boss_profile.nickname if boss_profile else '未设置昵称'
|
||||||
|
|
||||||
|
# 获取头像(相对URL)
|
||||||
|
touxiang_url = user_main.Avatar or ''
|
||||||
|
|
||||||
|
# 获取流水金额
|
||||||
|
if leixing == 1:
|
||||||
|
liushui_jine = shangjia.jinriliushui
|
||||||
|
else:
|
||||||
|
liushui_jine = shangjia.jinyueliushui
|
||||||
|
|
||||||
|
paihang_list.append({
|
||||||
|
'uid': user_main.UserUID,
|
||||||
|
'touxiang': touxiang_url,
|
||||||
|
'nicheng': shangjia_nicheng,
|
||||||
|
'jine': float(liushui_jine),
|
||||||
|
'mingci': index + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
return paihang_list, True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class GetAdminZhanghaoView(APIView):
|
||||||
|
"""
|
||||||
|
获取管理员账号列表接口
|
||||||
|
路径:/peizhi/adzhhq
|
||||||
|
方法:POST
|
||||||
|
权限:JWT Token + 管理员验证
|
||||||
|
参数:
|
||||||
|
- zhanghao: 当前登录的管理员账号(字符串)
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
# 🔥 1. 身份验证(100%按表模型)
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
# 获取当前用户的手机号(CharField,字符串类型)
|
||||||
|
current_phone = getattr(user, 'Phone', '')
|
||||||
|
if not current_phone:
|
||||||
|
return Response({
|
||||||
|
'code': 401,
|
||||||
|
'message': '用户手机号信息不完整',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 检查前端传递的账号是否匹配(都是字符串)
|
||||||
|
frontend_zhanghao = request.data.get('zhanghao', '').strip()
|
||||||
|
if frontend_zhanghao != current_phone:
|
||||||
|
return Response({
|
||||||
|
'code': 403,
|
||||||
|
'message': '账号不匹配,无权限访问',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# 🔥 2. 验证是否为管理员(user_type是CharField,值是'admin'字符串)
|
||||||
|
user_type = getattr(user, 'user_type', '')
|
||||||
|
if user_type != 'admin':
|
||||||
|
# 检查是否有管理员扩展表
|
||||||
|
if not hasattr(user, 'AdminProfile'):
|
||||||
|
return Response({
|
||||||
|
'code': 403,
|
||||||
|
'message': '无管理员权限',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# 🔥 3. 查询管理员账号列表(全部按字符串查询)
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# 🔥 查询所有管理员账号(user_type='admin' 字符串)
|
||||||
|
# 🔥 phone和yonghuid都是CharField,xiugaiid也是CharField
|
||||||
|
sql = """
|
||||||
|
SELECT
|
||||||
|
um.id,
|
||||||
|
um.phone,
|
||||||
|
um.yonghuid,
|
||||||
|
um.UserType,
|
||||||
|
COALESCE(COUNT(xjl.id), 0) as jilushuliang
|
||||||
|
FROM user_main um
|
||||||
|
LEFT JOIN xiugaijilu xjl ON um.phone = xjl.xiugaiid
|
||||||
|
WHERE um.UserType = 'admin'
|
||||||
|
GROUP BY um.id, um.phone, um.yonghuid, um.UserType
|
||||||
|
ORDER BY um.CreateTime DESC
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(sql)
|
||||||
|
results = cursor.fetchall()
|
||||||
|
|
||||||
|
# 🔥 4. 处理查询结果(全部保持字符串类型)
|
||||||
|
zhanghao_list = []
|
||||||
|
for row in results:
|
||||||
|
zhanghao_list.append({
|
||||||
|
#'id': row[0],
|
||||||
|
'id': str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
|
||||||
|
'zhanghao':str(row[1]) if row[1] else str(row[2]), # 🔥 保持字符串
|
||||||
|
'yonghuid': str(row[2]) if row[2] else '', # 🔥 保持字符串
|
||||||
|
'user_type': str(row[3]) if row[3] else '', # 🔥 保持字符串
|
||||||
|
'jilushuliang': int(row[4]) # 只有这个是数字
|
||||||
|
})
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'message': '获取成功',
|
||||||
|
'data': zhanghao_list
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取管理员账号列表失败: {str(e)}")
|
||||||
|
traceback.print_exc()
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': '系统错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
class GetXiugaiJiluView(APIView):
|
||||||
|
"""
|
||||||
|
获取修改记录列表接口
|
||||||
|
路径:/peizhi/adhqxg
|
||||||
|
方法:POST
|
||||||
|
权限:JWT Token + 管理员验证
|
||||||
|
参数:
|
||||||
|
- zhanghao: 当前登录的管理员账号(字符串)
|
||||||
|
- xiugaizhe_zhanghaoid: 选中的管理员账号ID(字符串,对应phone或yonghuid)
|
||||||
|
- page: 页码(整数)
|
||||||
|
- pagesize: 每页条数(整数)
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
# 🔥 1. 身份验证(100%按表模型)
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
# 获取当前用户的手机号(CharField,字符串)
|
||||||
|
current_phone = getattr(user, 'Phone', '')
|
||||||
|
if not current_phone:
|
||||||
|
return Response({
|
||||||
|
'code': 401,
|
||||||
|
'message': '用户手机号信息不完整',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
# 检查前端传递的账号是否匹配(字符串比较)
|
||||||
|
frontend_zhanghao = request.data.get('zhanghao', '')
|
||||||
|
if not isinstance(frontend_zhanghao, str):
|
||||||
|
frontend_zhanghao = str(frontend_zhanghao)
|
||||||
|
frontend_zhanghao = frontend_zhanghao.strip()
|
||||||
|
|
||||||
|
if frontend_zhanghao != current_phone:
|
||||||
|
return Response({
|
||||||
|
'code': 403,
|
||||||
|
'message': '账号不匹配,无权限访问',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# 🔥 2. 验证是否为管理员(user_type='admin'字符串)
|
||||||
|
user_type = getattr(user, 'user_type', '')
|
||||||
|
if user_type != 'admin':
|
||||||
|
if not hasattr(user, 'AdminProfile'):
|
||||||
|
return Response({
|
||||||
|
'code': 403,
|
||||||
|
'message': '无管理员权限',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# 🔥 3. 获取参数(100%按实际类型)
|
||||||
|
# 🔥 xiugaizhe_zhanghaoid 是字符串(对应xiugaiid字段,CharField)
|
||||||
|
xiugaizhe_zhanghaoid_param = request.data.get('xiugaizhe_zhanghaoid', '')
|
||||||
|
|
||||||
|
|
||||||
|
# 🔥 强制转为字符串(因为数据库是CharField)
|
||||||
|
if xiugaizhe_zhanghaoid_param is None:
|
||||||
|
xiugaizhe_zhanghaoid = ''
|
||||||
|
else:
|
||||||
|
# 无论如何都转为字符串
|
||||||
|
xiugaizhe_zhanghaoid = str(xiugaizhe_zhanghaoid_param).strip()
|
||||||
|
|
||||||
|
|
||||||
|
# 🔥 分页参数转为整数
|
||||||
|
try:
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
except:
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
pagesize = int(request.data.get('pagesize', 10))
|
||||||
|
except:
|
||||||
|
pagesize = 10
|
||||||
|
|
||||||
|
# 🔥 4. 参数验证
|
||||||
|
if not xiugaizhe_zhanghaoid:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '修改者账号ID不能为空',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if page < 1:
|
||||||
|
page = 1
|
||||||
|
if pagesize < 1 or pagesize > 50:
|
||||||
|
pagesize = 10
|
||||||
|
|
||||||
|
# 🔥 5. 高效查询(100%按表字段类型)
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
# 🔥 5.1 查询总记录数(xiugaiid是CharField,用字符串查询)
|
||||||
|
count_sql = """
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM xiugaijilu
|
||||||
|
WHERE xiugaiid = %s
|
||||||
|
"""
|
||||||
|
cursor.execute(count_sql, [xiugaizhe_zhanghaoid])
|
||||||
|
total_count = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
# 🔥 5.2 计算分页
|
||||||
|
offset = (page - 1) * pagesize
|
||||||
|
|
||||||
|
# 🔥 5.3 查询当前页数据(使用参数化查询,防止SQL注入)
|
||||||
|
data_sql = """
|
||||||
|
SELECT
|
||||||
|
xjl.id,
|
||||||
|
xjl.yonghuid, -- CharField
|
||||||
|
xjl.xiugaiid, -- CharField
|
||||||
|
xjl.leixing, -- IntegerField
|
||||||
|
xjl.xiugaitijiao, -- DecimalField
|
||||||
|
xjl.xiugaitijiaoq, -- DecimalField
|
||||||
|
xjl.jifen, -- IntegerField
|
||||||
|
xjl.yjifen, -- IntegerField
|
||||||
|
xjl.yajin, -- DecimalField
|
||||||
|
xjl.yyajin, -- DecimalField
|
||||||
|
xjl.shangjiayue, -- DecimalField
|
||||||
|
xjl.shangjiayueq, -- DecimalField
|
||||||
|
xjl.guanshiyue, -- DecimalField
|
||||||
|
xjl.guanshiyueq, -- DecimalField
|
||||||
|
xjl.huiyuants, -- IntegerField
|
||||||
|
xjl.huiyuan_id, -- CharField
|
||||||
|
xjl.qitashuoming, -- TextField
|
||||||
|
xjl.CreateTime, -- DateTimeField
|
||||||
|
um.avatar -- CharField
|
||||||
|
FROM xiugaijilu xjl
|
||||||
|
LEFT JOIN user_main um ON xjl.yonghuid = um.yonghuid
|
||||||
|
WHERE xjl.xiugaiid = %s
|
||||||
|
ORDER BY xjl.CreateTime DESC
|
||||||
|
LIMIT %s OFFSET %s
|
||||||
|
"""
|
||||||
|
|
||||||
|
cursor.execute(data_sql, [xiugaizhe_zhanghaoid, pagesize, offset])
|
||||||
|
records = cursor.fetchall()
|
||||||
|
|
||||||
|
# 🔥 6. 处理查询结果(严格按照字段类型处理)
|
||||||
|
jilu_list = []
|
||||||
|
for record in records:
|
||||||
|
# 获取修改者账号信息(都是字符串)
|
||||||
|
xiugaizhe_zhanghao = ''
|
||||||
|
with connection.cursor() as cursor2:
|
||||||
|
# 🔥 phone和yonghuid都是CharField,用字符串查询
|
||||||
|
cursor2.execute(
|
||||||
|
"SELECT phone, yonghuid FROM user_main WHERE phone = %s OR yonghuid = %s LIMIT 1",
|
||||||
|
[xiugaizhe_zhanghaoid, xiugaizhe_zhanghaoid]
|
||||||
|
)
|
||||||
|
xiugaizhe_info = cursor2.fetchone()
|
||||||
|
if xiugaizhe_info:
|
||||||
|
# 🔥 都是字符串
|
||||||
|
xiugaizhe_zhanghao = str(xiugaizhe_info[0]) if xiugaizhe_info[0] else str(xiugaizhe_info[1]) if \
|
||||||
|
xiugaizhe_info[1] else xiugaizhe_zhanghaoid
|
||||||
|
|
||||||
|
# 获取会员名称(如果有)
|
||||||
|
huiyuan_mingzi = ''
|
||||||
|
if record[15]: # huiyuan_id是CharField
|
||||||
|
with connection.cursor() as cursor3:
|
||||||
|
cursor3.execute(
|
||||||
|
"SELECT jieshao FROM huiyuan WHERE huiyuan_id = %s LIMIT 1",
|
||||||
|
[str(record[15])] # 🔥 转为字符串查询
|
||||||
|
)
|
||||||
|
huiyuan_info = cursor3.fetchone()
|
||||||
|
if huiyuan_info:
|
||||||
|
huiyuan_mingzi = str(huiyuan_info[0]) if huiyuan_info[0] else ''
|
||||||
|
|
||||||
|
# 🔥 严格按照前端字段名映射,保持正确类型
|
||||||
|
jilu_data = {
|
||||||
|
# 基础字段(字符串)
|
||||||
|
'id': record[0],
|
||||||
|
'beixiugai_yonghuid': str(record[1]) if record[1] else '', # yonghuid
|
||||||
|
'xiugaizhe_id': str(record[2]) if record[2] else '', # xiugaiid
|
||||||
|
'xiugaizhe_zhanghao': xiugaizhe_zhanghao, # 字符串
|
||||||
|
'beixiugai_yonghuleixing': record[3], # leixing是整数
|
||||||
|
|
||||||
|
# 打手字段(Decimal转float,Integer转int)
|
||||||
|
'xiugaihou_dashoutixianjine': float(record[4]) if record[4] is not None else 0.00,
|
||||||
|
'xiugaiqian_dashoutixianjine': float(record[5]) if record[5] is not None else 0.00,
|
||||||
|
'xiugaihou_dashoujifen': int(record[6]) if record[6] is not None else 0,
|
||||||
|
'xiugaiqian_dashoujifen': int(record[7]) if record[7] is not None else 0,
|
||||||
|
'xiugaihou_dashouyajin': float(record[8]) if record[8] is not None else 0.00,
|
||||||
|
'xiugaiqian_dashouyajin': float(record[9]) if record[9] is not None else 0.00,
|
||||||
|
|
||||||
|
# 商家字段
|
||||||
|
'xiugaihou_shangjiayue': float(record[10]) if record[10] is not None else 0.00,
|
||||||
|
'xiugaiqian_shangjiayue': float(record[11]) if record[11] is not None else 0.00,
|
||||||
|
|
||||||
|
# 管事字段
|
||||||
|
'xiugaihou_guanshiyue': float(record[12]) if record[12] is not None else 0.00,
|
||||||
|
'xiugaiqian_guanshiyue': float(record[13]) if record[13] is not None else 0.00,
|
||||||
|
|
||||||
|
# 会员字段
|
||||||
|
'tianjia_huiyuantianshu': int(record[14]) if record[14] is not None else 0,
|
||||||
|
'huiyuan_id': str(record[15]) if record[15] else '', # 字符串
|
||||||
|
'huiyuan_mingzi': huiyuan_mingzi, # 字符串
|
||||||
|
|
||||||
|
# 其他字段
|
||||||
|
'qita_shuoming': str(record[16]) if record[16] else '', # 字符串
|
||||||
|
'xiugai_time': record[17].strftime('%Y-%m-%d %H:%M:%S') if record[17] else None,
|
||||||
|
'touxiang': str(record[18]) if record[18] else '' # avatar是字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
jilu_list.append(jilu_data)
|
||||||
|
|
||||||
|
# 🔥 7. 分页控制
|
||||||
|
queried_records = len(jilu_list)
|
||||||
|
has_more = False
|
||||||
|
|
||||||
|
if queried_records == pagesize:
|
||||||
|
total_queried = (page - 1) * pagesize + queried_records
|
||||||
|
has_more = total_queried < total_count
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'message': '获取成功',
|
||||||
|
'data': {
|
||||||
|
'list': jilu_list,
|
||||||
|
'zongshuliang': total_count,
|
||||||
|
'dangqian_page': page,
|
||||||
|
'page_size': pagesize,
|
||||||
|
'has_more': has_more,
|
||||||
|
'shijichaxundediaoshuliang': queried_records
|
||||||
|
}
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"参数错误: {str(e)}")
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'message': '参数错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"获取修改记录失败: {str(e)}")
|
||||||
|
traceback.print_exc()
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': '系统错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
1032
config/views/merchant_links.py
Normal file
1032
config/views/merchant_links.py
Normal file
File diff suppressed because it is too large
Load Diff
486
config/views/merchant_templates.py
Normal file
486
config/views/merchant_templates.py
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import secrets
|
||||||
|
import hashlib
|
||||||
|
import threading
|
||||||
|
import urllib.parse
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import transaction, connection
|
||||||
|
from django.db.models import Q, F, Max, Prefetch
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status, permissions
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
|
||||||
|
|
||||||
|
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
|
||||||
|
from utils.weixin_broadcast import WeixinBroadcastSender
|
||||||
|
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||||||
|
# from utils.ip_security import *
|
||||||
|
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||||
|
|
||||||
|
from backend.utils import update_shangjia_daily
|
||||||
|
from utils.chat_utils import subscribe_merchant_link_chat
|
||||||
|
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||||
|
|
||||||
|
from ..models import (
|
||||||
|
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||||||
|
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||||||
|
MiniappScriptScene, MiniappScriptAutoReply,
|
||||||
|
)
|
||||||
|
|
||||||
|
from users.models import (
|
||||||
|
AdminProfile, UserDashou, UserBoss,
|
||||||
|
UserShangjia, UserGuanshi, UserZuzhang
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
from orders.models import CommissionRate, Order, MerchantOrderExt
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
|
||||||
|
from rank.models import Chenghao, DingdanBiaoqian
|
||||||
|
|
||||||
|
from products.models import ShangpinLeixing, Huiyuangoumai
|
||||||
|
|
||||||
|
from ..serializers import PopupConfigSerializer
|
||||||
|
import traceback
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ShangjiaMobanListView(APIView):
|
||||||
|
"""
|
||||||
|
商家订单模板列表接口
|
||||||
|
POST /peizhi/sjddmblb
|
||||||
|
权限: JWT认证
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
user = request.user
|
||||||
|
yonghu_id = user.UserUID
|
||||||
|
|
||||||
|
leixing_id = request.data.get('shangpinTypeId')
|
||||||
|
if not leixing_id:
|
||||||
|
return Response({'code': 400, 'msg': '商品类型ID不能为空', 'data': {}}, status=400)
|
||||||
|
|
||||||
|
get_type = int(request.data.get('getType', 2))
|
||||||
|
page = int(request.data.get('page', 1))
|
||||||
|
page_size = int(request.data.get('pageSize', 50))
|
||||||
|
|
||||||
|
keyword = request.data.get('keyword', '')
|
||||||
|
min_price = request.data.get('minPrice', '')
|
||||||
|
label_id = request.data.get('labelId', None)
|
||||||
|
if label_id is not None:
|
||||||
|
try:
|
||||||
|
label_id = int(label_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
label_id = None
|
||||||
|
|
||||||
|
query = Q(UserID=yonghu_id, ProductTypeID=leixing_id)
|
||||||
|
|
||||||
|
if get_type == 1: # 搜索模式
|
||||||
|
if keyword:
|
||||||
|
keyword = str(keyword).strip()
|
||||||
|
try:
|
||||||
|
mid = int(keyword)
|
||||||
|
query &= Q(id=mid) | Q(TemplateDesc__icontains=keyword)
|
||||||
|
except ValueError:
|
||||||
|
query &= Q(TemplateDesc__icontains=keyword)
|
||||||
|
if min_price:
|
||||||
|
try:
|
||||||
|
query &= Q(Price__gte=Decimal(str(min_price)))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if label_id:
|
||||||
|
query &= Q(TitleID=label_id)
|
||||||
|
|
||||||
|
mobans = ShangjiaMoban.query.filter(query).order_by('-id')
|
||||||
|
formatted = []
|
||||||
|
for moban in mobans:
|
||||||
|
texiao_json = ''
|
||||||
|
label_name = ''
|
||||||
|
if moban.TitleID:
|
||||||
|
try:
|
||||||
|
ch = Chenghao.query.get(id=moban.TitleID)
|
||||||
|
label_name = ch.mingcheng
|
||||||
|
texiao_json = ch.texiao_miaoshu or ''
|
||||||
|
except Chenghao.DoesNotExist:
|
||||||
|
pass
|
||||||
|
formatted.append({
|
||||||
|
'mobanId': moban.id,
|
||||||
|
'shangpinTypeId': moban.ProductTypeID,
|
||||||
|
'jieshao': moban.TemplateDesc,
|
||||||
|
'jiage': str(moban.Price),
|
||||||
|
'fabushuliang': moban.PublishedCount,
|
||||||
|
'commissionEnabled': moban.CommissionGrabEnabled,
|
||||||
|
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
|
||||||
|
'labelId': moban.TitleID,
|
||||||
|
'labelName': label_name,
|
||||||
|
'texiaoJson': texiao_json,
|
||||||
|
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if moban.CreateTime else ''
|
||||||
|
})
|
||||||
|
return Response({'code': 200, 'msg': '搜索成功', 'data': {'list': formatted, 'total': len(formatted), 'hasMore': False}})
|
||||||
|
|
||||||
|
# 分页模式
|
||||||
|
queryset = ShangjiaMoban.query.filter(query).order_by('-id')
|
||||||
|
paginator = Paginator(queryset, page_size)
|
||||||
|
if page < 1 or page > paginator.num_pages:
|
||||||
|
return Response({'code': 400, 'msg': '页码超出范围', 'data': {}}, status=400)
|
||||||
|
|
||||||
|
current_page = paginator.page(page)
|
||||||
|
formatted = []
|
||||||
|
for moban in current_page.object_list:
|
||||||
|
texiao_json = ''
|
||||||
|
label_name = ''
|
||||||
|
if moban.TitleID:
|
||||||
|
try:
|
||||||
|
ch = Chenghao.query.get(id=moban.TitleID)
|
||||||
|
label_name = ch.mingcheng
|
||||||
|
texiao_json = ch.texiao_miaoshu or ''
|
||||||
|
except Chenghao.DoesNotExist:
|
||||||
|
pass
|
||||||
|
formatted.append({
|
||||||
|
'mobanId': moban.id,
|
||||||
|
'shangpinTypeId': moban.ProductTypeID,
|
||||||
|
'jieshao': moban.TemplateDesc,
|
||||||
|
'jiage': str(moban.Price),
|
||||||
|
'fabushuliang': moban.PublishedCount,
|
||||||
|
'commissionEnabled': moban.CommissionGrabEnabled,
|
||||||
|
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
|
||||||
|
'labelId': moban.TitleID,
|
||||||
|
'labelName': label_name,
|
||||||
|
'texiaoJson': texiao_json,
|
||||||
|
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if moban.CreateTime else ''
|
||||||
|
})
|
||||||
|
|
||||||
|
has_more = current_page.has_next()
|
||||||
|
if len(formatted) < page_size:
|
||||||
|
has_more = False
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'msg': '获取成功',
|
||||||
|
'data': {
|
||||||
|
'list': formatted,
|
||||||
|
'total': paginator.count,
|
||||||
|
'hasMore': has_more,
|
||||||
|
'currentPage': page,
|
||||||
|
'pageSize': page_size,
|
||||||
|
'totalPages': paginator.num_pages
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取模板列表失败: {str(e)}", exc_info=True)
|
||||||
|
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ShangjiaTianjiaMobanView(APIView):
|
||||||
|
"""
|
||||||
|
商家添加订单模板接口
|
||||||
|
POST /peizhi/sjtjddmb
|
||||||
|
权限: JWT认证
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
# 1. 获取当前商家用户ID
|
||||||
|
user = request.user
|
||||||
|
yonghu_id = user.UserUID
|
||||||
|
|
||||||
|
# 2. 接收并清洗参数,避免类型错误
|
||||||
|
leixing_id = request.data.get('shangpinTypeId')
|
||||||
|
jieshao = request.data.get('jieshao', '').strip() if request.data.get('jieshao') else ''
|
||||||
|
jiage = request.data.get('jiage', '')
|
||||||
|
# 佣金相关
|
||||||
|
commission_enabled = request.data.get('commissionEnabled', False)
|
||||||
|
commission_value = request.data.get('commissionValue', None)
|
||||||
|
# 标签ID(称号ID)
|
||||||
|
label_id = request.data.get('labelId', None)
|
||||||
|
|
||||||
|
# 3. 必填验证
|
||||||
|
if not leixing_id or not jieshao or not jiage:
|
||||||
|
logger.warning(f"参数缺失 - leixing_id:{leixing_id}, jieshao:{jieshao}, jiage:{jiage}")
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'msg': '缺少必填字段(订单类型、介绍、价格)',
|
||||||
|
'data': {}
|
||||||
|
}, status=400)
|
||||||
|
|
||||||
|
# 4. 价格格式验证
|
||||||
|
try:
|
||||||
|
jiage_decimal = Decimal(str(jiage))
|
||||||
|
if jiage_decimal <= 0:
|
||||||
|
return Response({'code': 400, 'msg': '价格必须大于0', 'data': {}}, status=400)
|
||||||
|
if jiage_decimal > 10000:
|
||||||
|
return Response({'code': 400, 'msg': '价格不能超过10000元', 'data': {}}, status=400)
|
||||||
|
except (InvalidOperation, ValueError, TypeError):
|
||||||
|
return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400)
|
||||||
|
|
||||||
|
# 5. 介绍长度验证
|
||||||
|
if len(jieshao) > 100:
|
||||||
|
return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400)
|
||||||
|
|
||||||
|
# 6. 佣金验证(若启用)
|
||||||
|
CommissionGrabEnabled = False
|
||||||
|
CommissionPrice = None
|
||||||
|
if commission_enabled:
|
||||||
|
CommissionGrabEnabled = True
|
||||||
|
if not commission_value:
|
||||||
|
return Response({'code': 400, 'msg': '开启佣金时,佣金金额不能为空', 'data': {}}, status=400)
|
||||||
|
try:
|
||||||
|
comm_decimal = Decimal(str(commission_value))
|
||||||
|
if comm_decimal <= 0:
|
||||||
|
return Response({'code': 400, 'msg': '佣金必须大于0', 'data': {}}, status=400)
|
||||||
|
CommissionPrice = comm_decimal
|
||||||
|
except (InvalidOperation, ValueError, TypeError):
|
||||||
|
return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400)
|
||||||
|
|
||||||
|
# 7. 标签ID清洗(前端可能传数字或字符串)
|
||||||
|
cleaned_label_id = None
|
||||||
|
if label_id is not None and str(label_id).strip():
|
||||||
|
try:
|
||||||
|
cleaned_label_id = int(label_id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return Response({'code': 400, 'msg': '标签ID格式错误', 'data': {}}, status=400)
|
||||||
|
|
||||||
|
# 8. 验证商品类型是否存在且可用
|
||||||
|
try:
|
||||||
|
shangpin_obj = ShangpinLeixing.query.get(
|
||||||
|
id=leixing_id,
|
||||||
|
shenhezhuangtai=1 # 审核通过
|
||||||
|
)
|
||||||
|
except ShangpinLeixing.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '商品类型不存在或已禁用', 'data': {}}, status=404)
|
||||||
|
|
||||||
|
# 9. 检查同一用户、同一类型下是否已存在相同介绍的模板
|
||||||
|
if ShangjiaMoban.query.filter(
|
||||||
|
UserID=yonghu_id,
|
||||||
|
ProductTypeID=leixing_id,
|
||||||
|
TemplateDesc=jieshao
|
||||||
|
).exists():
|
||||||
|
return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400)
|
||||||
|
|
||||||
|
# 10. 执行数据库创建(事务)
|
||||||
|
with transaction.atomic():
|
||||||
|
moban = ShangjiaMoban.query.create(
|
||||||
|
UserID=yonghu_id,
|
||||||
|
ProductTypeID=leixing_id,
|
||||||
|
TemplateDesc=jieshao,
|
||||||
|
Price=jiage_decimal,
|
||||||
|
PublishedCount=0,
|
||||||
|
CommissionGrabEnabled=CommissionGrabEnabled,
|
||||||
|
CommissionPrice=CommissionPrice,
|
||||||
|
TitleID=cleaned_label_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# 11. 返回成功数据(字段与前端对齐)
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'msg': '模板创建成功',
|
||||||
|
'data': {
|
||||||
|
'mobanId': moban.id,
|
||||||
|
'shangpinTypeId': leixing_id,
|
||||||
|
'jieshao': moban.TemplateDesc,
|
||||||
|
'jiage': str(moban.Price),
|
||||||
|
'fabushuliang': moban.PublishedCount,
|
||||||
|
'createTime': moban.CreateTime.strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'commissionEnabled': moban.CommissionGrabEnabled,
|
||||||
|
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice is not None else None,
|
||||||
|
'labelId': moban.TitleID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 打印完整错误堆栈到日志
|
||||||
|
logger.error(f"添加模板失败: {str(e)}", exc_info=True)
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'msg': '服务器内部错误,添加模板失败,请联系管理员',
|
||||||
|
'data': {}
|
||||||
|
}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ShangjiaShanchuMobanView(APIView):
|
||||||
|
"""
|
||||||
|
删除模板
|
||||||
|
POST /peizhi/sjddmbsc
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
user = request.user
|
||||||
|
yonghu_id = user.UserUID
|
||||||
|
|
||||||
|
moban_id = request.data.get('mobanId')
|
||||||
|
leixing_id = request.data.get('shangpinTypeId')
|
||||||
|
|
||||||
|
if not all([moban_id, leixing_id]):
|
||||||
|
return Response({'code': 400, 'msg': '参数缺失', 'data': {}}, status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
moban = ShangjiaMoban.query.get(id=moban_id, UserID=yonghu_id, ProductTypeID=leixing_id)
|
||||||
|
except ShangjiaMoban.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '模板不存在', 'data': {}}, status=404)
|
||||||
|
|
||||||
|
# 强制硬删除
|
||||||
|
moban.delete()
|
||||||
|
|
||||||
|
return Response({'code': 200, 'msg': '删除成功', 'data': {'mobanId': moban_id, 'shangpinTypeId': leixing_id}})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"删除模板失败: {str(e)}", exc_info=True)
|
||||||
|
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ShangjiaGengxinMobanView(APIView):
|
||||||
|
"""
|
||||||
|
商家更新订单模板接口
|
||||||
|
POST /peizhi/sjddmbgx
|
||||||
|
权限: JWT认证
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
user = request.user
|
||||||
|
yonghu_id = user.UserUID
|
||||||
|
|
||||||
|
moban_id = request.data.get('mobanId')
|
||||||
|
leixing_id = request.data.get('shangpinTypeId')
|
||||||
|
new_jieshao = request.data.get('jieshao', '').strip()
|
||||||
|
new_jiage = request.data.get('jiage', '').strip()
|
||||||
|
# 新增字段
|
||||||
|
commission_enabled = request.data.get('commissionEnabled')
|
||||||
|
commission_value = request.data.get('commissionValue')
|
||||||
|
label_id = request.data.get('labelId')
|
||||||
|
|
||||||
|
if not all([moban_id, leixing_id]):
|
||||||
|
return Response({'code': 400, 'msg': '模板ID和商品类型ID不能为空', 'data': {}}, status=400)
|
||||||
|
|
||||||
|
# 查询模板
|
||||||
|
try:
|
||||||
|
moban = ShangjiaMoban.query.get(
|
||||||
|
id=moban_id,
|
||||||
|
UserID=yonghu_id,
|
||||||
|
ProductTypeID=leixing_id
|
||||||
|
)
|
||||||
|
except ShangjiaMoban.DoesNotExist:
|
||||||
|
return Response({'code': 404, 'msg': '模板不存在或无权修改', 'data': {}}, status=404)
|
||||||
|
|
||||||
|
update_fields = []
|
||||||
|
|
||||||
|
# 修改介绍
|
||||||
|
if new_jieshao:
|
||||||
|
if len(new_jieshao) > 100:
|
||||||
|
return Response({'code': 400, 'msg': '订单介绍不能超过100字', 'data': {}}, status=400)
|
||||||
|
if new_jieshao != moban.TemplateDesc:
|
||||||
|
moban.TemplateDesc = new_jieshao
|
||||||
|
update_fields.append('TemplateDesc')
|
||||||
|
|
||||||
|
# 修改价格
|
||||||
|
if new_jiage:
|
||||||
|
try:
|
||||||
|
jiage_decimal = Decimal(new_jiage)
|
||||||
|
if jiage_decimal <= 0 or jiage_decimal > 10000:
|
||||||
|
return Response({'code': 400, 'msg': '价格必须在0.1-10000元之间', 'data': {}}, status=400)
|
||||||
|
if jiage_decimal != moban.Price:
|
||||||
|
moban.Price = jiage_decimal
|
||||||
|
update_fields.append('Price')
|
||||||
|
except (InvalidOperation, ValueError, TypeError):
|
||||||
|
return Response({'code': 400, 'msg': '价格格式错误', 'data': {}}, status=400)
|
||||||
|
|
||||||
|
# 修改标签
|
||||||
|
if label_id is not None: # 允许传空字符串或不传,传0表示清空
|
||||||
|
new_label = int(label_id) if label_id else None
|
||||||
|
if new_label != moban.TitleID:
|
||||||
|
moban.TitleID = new_label
|
||||||
|
update_fields.append('TitleID')
|
||||||
|
|
||||||
|
# 修改佣金设置
|
||||||
|
if commission_enabled is not None:
|
||||||
|
enabled = bool(commission_enabled)
|
||||||
|
if enabled:
|
||||||
|
if not commission_value:
|
||||||
|
return Response({'code': 400, 'msg': '佣金价格不能为空', 'data': {}}, status=400)
|
||||||
|
try:
|
||||||
|
comm_decimal = Decimal(commission_value)
|
||||||
|
if comm_decimal < 0.1 or comm_decimal > 50:
|
||||||
|
return Response({'code': 400, 'msg': '佣金需在0.1-50元之间', 'data': {}}, status=400)
|
||||||
|
if moban.CommissionGrabEnabled != True or moban.CommissionPrice != comm_decimal:
|
||||||
|
moban.CommissionGrabEnabled = True
|
||||||
|
moban.CommissionPrice = comm_decimal
|
||||||
|
update_fields.extend(['CommissionGrabEnabled', 'CommissionPrice'])
|
||||||
|
except (InvalidOperation, ValueError, TypeError):
|
||||||
|
return Response({'code': 400, 'msg': '佣金格式错误', 'data': {}}, status=400)
|
||||||
|
else:
|
||||||
|
if moban.CommissionGrabEnabled != False or moban.CommissionPrice is not None:
|
||||||
|
moban.CommissionGrabEnabled = False
|
||||||
|
moban.CommissionPrice = None
|
||||||
|
update_fields.extend(['CommissionGrabEnabled', 'CommissionPrice'])
|
||||||
|
|
||||||
|
if not update_fields:
|
||||||
|
return Response({'code': 400, 'msg': '没有修改内容', 'data': {}}, status=400)
|
||||||
|
|
||||||
|
# 检查介绍唯一性
|
||||||
|
if 'TemplateDesc' in update_fields:
|
||||||
|
if ShangjiaMoban.query.filter(
|
||||||
|
UserID=yonghu_id,
|
||||||
|
ProductTypeID=leixing_id,
|
||||||
|
TemplateDesc=moban.TemplateDesc
|
||||||
|
).exclude(id=moban_id).exists():
|
||||||
|
return Response({'code': 400, 'msg': '已存在相同介绍的模板', 'data': {}}, status=400)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
moban.save(update_fields=update_fields)
|
||||||
|
|
||||||
|
# 返回最新数据
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'msg': '模板修改成功',
|
||||||
|
'data': {
|
||||||
|
'mobanId': moban.id,
|
||||||
|
'shangpinTypeId': moban.ProductTypeID,
|
||||||
|
'jieshao': moban.TemplateDesc,
|
||||||
|
'jiage': str(moban.Price),
|
||||||
|
'fabushuliang': moban.PublishedCount,
|
||||||
|
'commissionEnabled': moban.CommissionGrabEnabled,
|
||||||
|
'commissionValue': str(moban.CommissionPrice) if moban.CommissionPrice else None,
|
||||||
|
'labelId': moban.TitleID,
|
||||||
|
'updateTime': moban.UpdateTime.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.error(f"修改商家模板失败: {str(e)}", exc_info=True)
|
||||||
|
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
320
config/views/miniapp_config.py
Normal file
320
config/views/miniapp_config.py
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import secrets
|
||||||
|
import hashlib
|
||||||
|
import threading
|
||||||
|
import urllib.parse
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import transaction, connection
|
||||||
|
from django.db.models import Q, F, Max, Prefetch
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status, permissions
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
|
||||||
|
|
||||||
|
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
|
||||||
|
from utils.weixin_broadcast import WeixinBroadcastSender
|
||||||
|
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||||||
|
# from utils.ip_security import *
|
||||||
|
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||||
|
|
||||||
|
from backend.utils import update_shangjia_daily
|
||||||
|
from utils.chat_utils import subscribe_merchant_link_chat
|
||||||
|
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||||
|
|
||||||
|
from ..models import (
|
||||||
|
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||||||
|
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||||||
|
MiniappScriptScene, MiniappScriptAutoReply,
|
||||||
|
)
|
||||||
|
|
||||||
|
from users.models import (
|
||||||
|
AdminProfile, UserDashou, UserBoss,
|
||||||
|
UserShangjia, UserGuanshi, UserZuzhang
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
from orders.models import CommissionRate, Order, MerchantOrderExt
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
|
||||||
|
from rank.models import Chenghao, DingdanBiaoqian
|
||||||
|
|
||||||
|
from products.models import ShangpinLeixing, Huiyuangoumai
|
||||||
|
|
||||||
|
from ..serializers import PopupConfigSerializer
|
||||||
|
import traceback
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class GetDynamicConfigView(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
from jituan.services.display_config import get_tupianpeizhi_url
|
||||||
|
|
||||||
|
from jituan.services.miniapp_assets import build_miniapp_assets_payload
|
||||||
|
|
||||||
|
cos_oss_url = getattr(settings, 'COS_DOMAIN', '')
|
||||||
|
if cos_oss_url and not cos_oss_url.endswith('/'):
|
||||||
|
cos_oss_url += '/'
|
||||||
|
cos_bucket = getattr(settings, 'COS_BUCKET', '')
|
||||||
|
cos_region = getattr(settings, 'COS_REGION', 'ap-shanghai')
|
||||||
|
goeasy_host = 'hangzhou.goeasy.io'
|
||||||
|
goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '')
|
||||||
|
kefu_link = getattr(settings, 'KEFU_LINK', '')
|
||||||
|
kefu_enterprise_id = getattr(settings, 'KEFU_ENTERPRISE_ID', '')
|
||||||
|
rule_img = get_tupianpeizhi_url(request, 1)
|
||||||
|
avatar_img = get_tupianpeizhi_url(request, 2)
|
||||||
|
morentouxiang = avatar_img or 'beijing/morentouxiang.jpg'
|
||||||
|
dashouguize = rule_img or 'a_long/dashouguize.jpg'
|
||||||
|
|
||||||
|
# 构造返回数据
|
||||||
|
data = {
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"cos": {
|
||||||
|
"bucket": cos_bucket,
|
||||||
|
"region": cos_region,
|
||||||
|
"ossImageUrl": cos_oss_url,
|
||||||
|
"uploadPathPrefix": "order/"
|
||||||
|
},
|
||||||
|
"goEasy": {
|
||||||
|
"host": goeasy_host,
|
||||||
|
"appkey": goeasy_appkey
|
||||||
|
},
|
||||||
|
"otherConfig": {
|
||||||
|
"morentouxiang": morentouxiang,
|
||||||
|
"dashouguize": dashouguize
|
||||||
|
},
|
||||||
|
"kefu": {
|
||||||
|
"link": kefu_link,
|
||||||
|
"enterpriseId": kefu_enterprise_id
|
||||||
|
},
|
||||||
|
"miniappAssets": build_miniapp_assets_payload(request),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取动态配置失败: {str(e)}", exc_info=True)
|
||||||
|
return Response({"code": 500, "msg": "服务器内部错误"}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class HaibaoPeizhiView(APIView):
|
||||||
|
"""小程序推广海报背景配置(按俱乐部)。"""
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
parser_classes = [JSONParser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
from jituan.services.display_config import _display_club_id
|
||||||
|
from jituan.services.miniapp_assets import get_poster_payload
|
||||||
|
|
||||||
|
club_id = _display_club_id(request)
|
||||||
|
poster = get_poster_payload(club_id)
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'success',
|
||||||
|
'data': {
|
||||||
|
'club_id': club_id,
|
||||||
|
'poster': poster,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('获取海报配置失败: %s', e, exc_info=True)
|
||||||
|
return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class PopupConfigView(APIView):
|
||||||
|
"""
|
||||||
|
获取指定页面的弹窗配置
|
||||||
|
请求格式:POST { "pageKey": "dashouzhongxin" }
|
||||||
|
返回格式:{ "code": 0, "data": { "serverTime": "...", "popups": [...] } }
|
||||||
|
"""
|
||||||
|
authentication_classes = []
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
# 获取参数 pageKey
|
||||||
|
page_key = request.data.get('pageKey')
|
||||||
|
if not page_key:
|
||||||
|
return Response({
|
||||||
|
'code': 400,
|
||||||
|
'msg': '缺少参数 pageKey'
|
||||||
|
}, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 3. 查询页面配置(按俱乐部)
|
||||||
|
from jituan.services.display_config import get_gonggao_content
|
||||||
|
from jituan.services.club_context import resolve_club_id_from_request
|
||||||
|
|
||||||
|
club_id = resolve_club_id_from_request(request)
|
||||||
|
try:
|
||||||
|
popup_page = PopupPage.query.get(club_id=club_id, page_key=page_key, is_active=True)
|
||||||
|
except PopupPage.DoesNotExist:
|
||||||
|
# 该页面没有配置弹窗,返回空
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'serverTime': timezone.now().isoformat(),
|
||||||
|
'popups': []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# 4. 查询该页面下所有启用的弹窗(且在有效时间范围内)
|
||||||
|
now = timezone.now()
|
||||||
|
popups = PopupConfig.query.filter(
|
||||||
|
page=popup_page,
|
||||||
|
is_active=True,
|
||||||
|
start_time__lte=now,
|
||||||
|
end_time__gte=now
|
||||||
|
).prefetch_related('images') # 预加载图片,避免N+1查询
|
||||||
|
# 按 sort_order 排序(model Meta 已定义)
|
||||||
|
|
||||||
|
# 5. 序列化
|
||||||
|
serializer = PopupConfigSerializer(popups, many=True)
|
||||||
|
|
||||||
|
# 6. 返回数据
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'serverTime': now.isoformat(),
|
||||||
|
'ignore_user_mute': popup_page.ignore_user_mute, # 新增
|
||||||
|
'popups': serializer.data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class GetWithdrawModeView(APIView):
|
||||||
|
"""
|
||||||
|
获取提现模式配置
|
||||||
|
前端请求 /peizhi/hqtxzsym
|
||||||
|
返回格式: { "code": 0, "data": { "mode": 1 or 2 } }
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated] # JWT 认证
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
from jituan.services.club_context import resolve_club_id_from_request
|
||||||
|
from jituan.services.club_user import get_user_club_id
|
||||||
|
from jituan.services.withdraw_config import get_withdraw_mode
|
||||||
|
|
||||||
|
user = getattr(request, 'user', None)
|
||||||
|
club_id = get_user_club_id(user) if user and getattr(user, 'is_authenticated', False) else None
|
||||||
|
if not club_id:
|
||||||
|
club_id = resolve_club_id_from_request(request)
|
||||||
|
mode = get_withdraw_mode(club_id)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'mode': mode,
|
||||||
|
'club_id': club_id,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CheckPhoneAuthView(APIView):
|
||||||
|
"""
|
||||||
|
判断当前登录用户是否需要强制手机号认证
|
||||||
|
|
||||||
|
规则:
|
||||||
|
- 已认证过手机号(PhoneVerified=True) → 不需要
|
||||||
|
- 未认证时,满足以下任一条件 → 需要认证:
|
||||||
|
打手:有会员购买记录,或押金 > 0
|
||||||
|
商家:已是商家(存在商家扩展表)
|
||||||
|
管事:已是管事(存在管事扩展表)
|
||||||
|
组长:已是组长(存在组长扩展表)
|
||||||
|
老板:有下单记录(alldingdan > 0)
|
||||||
|
子客服:已是有效商家客服(merchant_staff_member 活跃)
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
if getattr(user, 'PhoneVerified', False):
|
||||||
|
return Response({'code': 0, 'need_auth': False})
|
||||||
|
|
||||||
|
if self._dashou_meets_threshold(user):
|
||||||
|
return Response({'code': 0, 'need_auth': True})
|
||||||
|
if self._shangjia_meets_threshold(user):
|
||||||
|
return Response({'code': 0, 'need_auth': True})
|
||||||
|
if self._guanshi_meets_threshold(user):
|
||||||
|
return Response({'code': 0, 'need_auth': True})
|
||||||
|
if self._zuzhang_meets_threshold(user):
|
||||||
|
return Response({'code': 0, 'need_auth': True})
|
||||||
|
if self._boss_meets_threshold(user):
|
||||||
|
return Response({'code': 0, 'need_auth': True})
|
||||||
|
if self._staff_meets_threshold(user):
|
||||||
|
return Response({'code': 0, 'need_auth': True})
|
||||||
|
|
||||||
|
return Response({'code': 0, 'need_auth': False})
|
||||||
|
|
||||||
|
def _staff_meets_threshold(self, user):
|
||||||
|
"""子客服:已是有效商家客服"""
|
||||||
|
try:
|
||||||
|
from merchant_ops.services.authz import get_active_staff_member
|
||||||
|
return get_active_staff_member(user) is not None
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _dashou_meets_threshold(self, user):
|
||||||
|
"""打手:有会员记录、押金 > 0、或有过接单/成交订单"""
|
||||||
|
try:
|
||||||
|
dashou = UserDashou.query.get(user=user)
|
||||||
|
except UserDashou.DoesNotExist:
|
||||||
|
return False
|
||||||
|
if dashou.yajin and dashou.yajin > 0:
|
||||||
|
return True
|
||||||
|
from jituan.services.club_context import resolve_effective_club_id
|
||||||
|
club_id = resolve_effective_club_id(self.request, user)
|
||||||
|
if Huiyuangoumai.query.filter(yonghu_id=user.UserUID, club_id=club_id).exists():
|
||||||
|
return True
|
||||||
|
if (dashou.jiedanzongliang or 0) > 0 or (dashou.chengjiaozongliang or 0) > 0:
|
||||||
|
return True
|
||||||
|
return Order.query.filter(PlayerID=user.UserUID).exists()
|
||||||
|
|
||||||
|
def _shangjia_meets_threshold(self, user):
|
||||||
|
"""商家:已是商家身份"""
|
||||||
|
return UserShangjia.query.filter(user=user).exists()
|
||||||
|
|
||||||
|
def _guanshi_meets_threshold(self, user):
|
||||||
|
"""管事:已是管事身份"""
|
||||||
|
return UserGuanshi.query.filter(user=user).exists()
|
||||||
|
|
||||||
|
def _zuzhang_meets_threshold(self, user):
|
||||||
|
"""组长:已是组长身份"""
|
||||||
|
return UserZuzhang.query.filter(user=user).exists()
|
||||||
|
|
||||||
|
def _boss_meets_threshold(self, user):
|
||||||
|
"""老板:有下单记录"""
|
||||||
|
try:
|
||||||
|
boss = UserBoss.query.get(user=user)
|
||||||
|
except UserBoss.DoesNotExist:
|
||||||
|
return False
|
||||||
|
return (boss.alldingdan or 0) > 0
|
||||||
|
|
||||||
|
|
||||||
553
config/views/qrcode_poster.py
Normal file
553
config/views/qrcode_poster.py
Normal file
@@ -0,0 +1,553 @@
|
|||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import secrets
|
||||||
|
import hashlib
|
||||||
|
import threading
|
||||||
|
import urllib.parse
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import transaction, connection
|
||||||
|
from django.db.models import Q, F, Max, Prefetch
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status, permissions
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
|
||||||
|
|
||||||
|
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
|
||||||
|
from utils.weixin_broadcast import WeixinBroadcastSender
|
||||||
|
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||||||
|
# from utils.ip_security import *
|
||||||
|
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||||
|
|
||||||
|
from backend.utils import update_shangjia_daily
|
||||||
|
from utils.chat_utils import subscribe_merchant_link_chat
|
||||||
|
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||||
|
|
||||||
|
from ..models import (
|
||||||
|
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||||||
|
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||||||
|
MiniappScriptScene, MiniappScriptAutoReply,
|
||||||
|
)
|
||||||
|
|
||||||
|
from users.models import (
|
||||||
|
AdminProfile, UserDashou, UserBoss,
|
||||||
|
UserShangjia, UserGuanshi, UserZuzhang
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
from orders.models import CommissionRate, Order, MerchantOrderExt
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
|
||||||
|
from rank.models import Chenghao, DingdanBiaoqian
|
||||||
|
|
||||||
|
from products.models import ShangpinLeixing, Huiyuangoumai
|
||||||
|
|
||||||
|
from ..serializers import PopupConfigSerializer
|
||||||
|
import traceback
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class GuanZhuAListView(APIView):
|
||||||
|
"""
|
||||||
|
获取关注阿龙页面图片列表
|
||||||
|
POST /peizhi/gzal
|
||||||
|
需要JWT认证
|
||||||
|
返回:
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"msg": "success",
|
||||||
|
"data": {
|
||||||
|
"images": ["相对URL1", "相对URL2", ...],
|
||||||
|
"last_update": "2025-03-16 12:34:56",
|
||||||
|
"visit_count": 123
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
try:
|
||||||
|
from jituan.services.club_context import resolve_effective_club_id
|
||||||
|
|
||||||
|
club_id = resolve_effective_club_id(request, request.user)
|
||||||
|
qs = Tupianpeizhi.query.filter(ImageType=3, club_id=club_id)
|
||||||
|
if not qs.exists():
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'success',
|
||||||
|
'data': {
|
||||||
|
'images': [],
|
||||||
|
'last_update': None,
|
||||||
|
'visit_count': 0
|
||||||
|
}
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
# 获取所有图片的相对URL
|
||||||
|
images = list(qs.values_list('ImageURL', flat=True))
|
||||||
|
|
||||||
|
# 获取最新更新时间(取所有记录的UpdateTime最大值)
|
||||||
|
last_update = qs.aggregate(UpdateTime__max=Max('UpdateTime'))['UpdateTime__max']
|
||||||
|
last_update_str = last_update.strftime('%Y-%m-%d %H:%M:%S') if last_update else None
|
||||||
|
|
||||||
|
# 原子性增加访问次数
|
||||||
|
# 先更新所有记录,再获取更新后的最大次数
|
||||||
|
qs.update(AccessCount=F('AccessCount') + 1)
|
||||||
|
visit_count = qs.aggregate(access_count__max=Max('AccessCount'))['access_count__max'] or 0
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'msg': 'success',
|
||||||
|
'data': {
|
||||||
|
'images': images,
|
||||||
|
'last_update': last_update_str,
|
||||||
|
'visit_count': visit_count
|
||||||
|
}
|
||||||
|
}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取关注阿龙图片列表失败: {e}", exc_info=True)
|
||||||
|
return Response({
|
||||||
|
'code': 1,
|
||||||
|
'msg': '服务器内部错误',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#不用缓存,一天2000次
|
||||||
|
class GuanshiQRCodeView(APIView):
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
logger.info("===== 开始生成管事二维码 =====")
|
||||||
|
current_user = request.user
|
||||||
|
logger.info(f"当前用户ID: {current_user.UserUID}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from jituan.services.club_context import resolve_effective_club_id
|
||||||
|
from utils.weixin_token import get_club_mini_access_token, is_weixin_token_invalid
|
||||||
|
|
||||||
|
club_id = resolve_effective_club_id(request, current_user)
|
||||||
|
try:
|
||||||
|
guanshi = current_user.GuanshiProfile
|
||||||
|
logger.info(f"管事扩展表查询成功,状态: {guanshi.zhuangtai}")
|
||||||
|
except UserGuanshi.DoesNotExist:
|
||||||
|
logger.warning(f"用户 {current_user.UserUID} 不是管事身份")
|
||||||
|
return Response(
|
||||||
|
{'code': 403, 'message': '当前用户不是管事身份', 'data': None},
|
||||||
|
status=status.HTTP_403_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
if guanshi.zhuangtai != 1:
|
||||||
|
logger.warning(f"用户 {current_user.UserUID} 管事状态异常: {guanshi.zhuangtai}")
|
||||||
|
return Response(
|
||||||
|
{'code': 403, 'message': '管事账号状态异常', 'data': None},
|
||||||
|
status=status.HTTP_403_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
invite_code = guanshi.yaoqingma
|
||||||
|
if not invite_code:
|
||||||
|
logger.info(f"用户 {current_user.UserUID} 无邀请码,开始生成")
|
||||||
|
invite_code = CreateInvitationCode(str(current_user.UserUID))
|
||||||
|
if not VerifyInvitationCode(invite_code):
|
||||||
|
logger.error(f"邀请码唯一性校验失败,用户: {current_user.UserUID}")
|
||||||
|
return Response(
|
||||||
|
{'code': 500, 'message': '邀请码生成失败', 'data': None},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
guanshi.yaoqingma = invite_code
|
||||||
|
guanshi.save(update_fields=['yaoqingma'])
|
||||||
|
logger.info(f"邀请码生成成功: {invite_code}")
|
||||||
|
else:
|
||||||
|
logger.info(f"使用已有邀请码: {invite_code}")
|
||||||
|
|
||||||
|
# 按俱乐部使用对应小程序 appid 的 access_token
|
||||||
|
access_token = get_club_mini_access_token(club_id)
|
||||||
|
if not access_token:
|
||||||
|
logger.error("获取 access_token 失败 club_id=%s", club_id)
|
||||||
|
return Response(
|
||||||
|
{'code': 500, 'message': '获取微信access_token失败', 'data': None},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
logger.info(f"获取到 access_token (前10位): {access_token[:10]}... club={club_id}")
|
||||||
|
|
||||||
|
encoded_invite = urllib.parse.quote(invite_code, safe='')
|
||||||
|
# 新小程序:打手端 Tab「我的」;旧码 dashouduan 由小程序兼容页转发
|
||||||
|
page_path = f'pages/fighter/fighter?inviteCode={encoded_invite}'
|
||||||
|
wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
|
||||||
|
post_data = {
|
||||||
|
'path': page_path,
|
||||||
|
'width': 430,
|
||||||
|
'auto_color': False,
|
||||||
|
'line_color': {'r': 0, 'g': 0, 'b': 0}
|
||||||
|
}
|
||||||
|
logger.info(f"调用微信接口生成小程序码,path: {page_path}")
|
||||||
|
|
||||||
|
def _request_qr(token):
|
||||||
|
try:
|
||||||
|
return requests.post(
|
||||||
|
f'https://api.weixin.qq.com/wxa/getwxacode?access_token={token}',
|
||||||
|
json=post_data, timeout=10,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"请求微信接口异常: {str(e)}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
wx_resp = _request_qr(access_token)
|
||||||
|
if wx_resp is None:
|
||||||
|
return Response(
|
||||||
|
{'code': 500, 'message': '调用微信服务失败', 'data': None},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
if wx_resp.headers.get('content-type', '').startswith('application/json'):
|
||||||
|
try:
|
||||||
|
err = wx_resp.json()
|
||||||
|
if is_weixin_token_invalid(err.get('errcode'), err.get('errmsg', '')):
|
||||||
|
access_token = get_club_mini_access_token(club_id, force_refresh=True)
|
||||||
|
if access_token:
|
||||||
|
wx_resp = _request_qr(access_token)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
logger.info(f"微信接口响应状态码: {wx_resp.status_code}")
|
||||||
|
|
||||||
|
if wx_resp.status_code != 200:
|
||||||
|
error_info = {}
|
||||||
|
if wx_resp.headers.get('content-type') == 'application/json':
|
||||||
|
try:
|
||||||
|
error_info = wx_resp.json()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
errmsg = error_info.get('errmsg', '未知错误')
|
||||||
|
logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}")
|
||||||
|
if 'errcode' in error_info:
|
||||||
|
logger.error(f"微信错误码: {error_info.get('errcode')}")
|
||||||
|
return Response(
|
||||||
|
{'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
content_type = wx_resp.headers.get('content-type', '')
|
||||||
|
if 'image' not in content_type:
|
||||||
|
try:
|
||||||
|
error_info = wx_resp.json()
|
||||||
|
errmsg = error_info.get('errmsg', '未知错误')
|
||||||
|
logger.error(f"微信接口返回非图片内容: {errmsg}")
|
||||||
|
return Response(
|
||||||
|
{'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
logger.error("微信接口返回未知内容,无法解析为JSON")
|
||||||
|
return Response(
|
||||||
|
{'code': 500, 'message': '生成二维码失败', 'data': None},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
timestamp = int(time.time())
|
||||||
|
filename = f"{current_user.UserUID}_{timestamp}.png"
|
||||||
|
oss_path = f"erweima/guanshi/{club_id}/{filename}"
|
||||||
|
file_obj = io.BytesIO(wx_resp.content)
|
||||||
|
logger.info(f"开始上传图片到COS: {oss_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
full_url = upload_to_oss(file_obj, oss_path)
|
||||||
|
logger.info(f"上传成功,完整URL: {full_url}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"上传到COS异常: {str(e)}", exc_info=True)
|
||||||
|
return Response(
|
||||||
|
{'code': 500, 'message': '图片上传失败', 'data': None},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
if not full_url:
|
||||||
|
logger.error("upload_to_oss 返回了空 URL")
|
||||||
|
return Response(
|
||||||
|
{'code': 500, 'message': '图片上传失败', 'data': None},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
cos_domain = getattr(settings, 'COS_DOMAIN', '')
|
||||||
|
if cos_domain and full_url.startswith(cos_domain):
|
||||||
|
relative_path = full_url[len(cos_domain):].lstrip('/')
|
||||||
|
else:
|
||||||
|
relative_path = oss_path
|
||||||
|
logger.info(f"相对路径: {relative_path}")
|
||||||
|
|
||||||
|
old_url = guanshi.invite_qrcode_url
|
||||||
|
if old_url:
|
||||||
|
logger.info(f"删除旧二维码: {old_url}")
|
||||||
|
try:
|
||||||
|
delete_from_oss(old_url)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"删除旧二维码失败(不影响主流程): {str(e)}")
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
guanshi.invite_qrcode_url = relative_path
|
||||||
|
guanshi.save(update_fields=['invite_qrcode_url'])
|
||||||
|
logger.info("数据库更新成功")
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'message': 'success',
|
||||||
|
'data': {'url': relative_path}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"生成二维码过程中发生未捕获异常: {str(e)}", exc_info=True)
|
||||||
|
return Response(
|
||||||
|
{'code': 500, 'message': '服务器内部错误', 'data': None},
|
||||||
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_fresh_access_token(self):
|
||||||
|
"""
|
||||||
|
直接请求微信接口获取新的 access_token,不缓存,保证最新。
|
||||||
|
"""
|
||||||
|
appid = getattr(settings, 'WEIXIN_APPID', '')
|
||||||
|
secret = getattr(settings, 'WEIXIN_SECRET', '')
|
||||||
|
if not appid or not secret:
|
||||||
|
logger.error("微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置")
|
||||||
|
return None
|
||||||
|
|
||||||
|
url = 'https://api.weixin.qq.com/cgi-bin/token'
|
||||||
|
params = {
|
||||||
|
'grant_type': 'client_credential',
|
||||||
|
'appid': appid,
|
||||||
|
'secret': secret
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
logger.info("正在请求微信接口获取最新 access_token...")
|
||||||
|
resp = requests.get(url, params=params, timeout=5)
|
||||||
|
data = resp.json()
|
||||||
|
new_token = data.get('access_token')
|
||||||
|
if new_token:
|
||||||
|
logger.info("成功获取最新 access_token")
|
||||||
|
return new_token
|
||||||
|
else:
|
||||||
|
errcode = data.get('errcode', '未知')
|
||||||
|
errmsg = data.get('errmsg', '未知错误')
|
||||||
|
logger.error(f"获取 access_token 失败,errcode={errcode}, errmsg={errmsg}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"请求微信 access_token 接口异常: {str(e)}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ZuzhangHaibaoView(APIView):
|
||||||
|
"""
|
||||||
|
组长专属海报二维码生成接口
|
||||||
|
路径:/peizhi/zuzhanghb
|
||||||
|
方法:POST
|
||||||
|
权限:JWT认证,仅限组长身份
|
||||||
|
返回:
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"url": "erweima/zuzhang/1000001_1234567890.png",
|
||||||
|
"yaoqingma": "ABC123XYZ"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
current_user = request.user
|
||||||
|
from jituan.services.club_context import resolve_effective_club_id
|
||||||
|
from utils.weixin_token import get_club_mini_access_token
|
||||||
|
|
||||||
|
club_id = resolve_effective_club_id(request, current_user)
|
||||||
|
|
||||||
|
# 1. 验证组长身份
|
||||||
|
try:
|
||||||
|
zuzhang = current_user.ZuzhangProfile
|
||||||
|
except UserZuzhang.DoesNotExist:
|
||||||
|
return Response({
|
||||||
|
'code': 403,
|
||||||
|
'message': '当前用户不是组长身份',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# 2. 验证组长账号状态
|
||||||
|
if zuzhang.zhuangtai != 1:
|
||||||
|
return Response({
|
||||||
|
'code': 403,
|
||||||
|
'message': '组长账号状态异常,无法生成海报',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
# 3. 获取或生成邀请码
|
||||||
|
invite_code = zuzhang.yaoqingma
|
||||||
|
if not invite_code:
|
||||||
|
invite_code = CreateInvitationCode(str(current_user.UserUID))
|
||||||
|
if not VerifyInvitationCode(invite_code):
|
||||||
|
logger.error(f"组长邀请码生成失败,用户: {current_user.UserUID}")
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': '邀请码生成失败',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
zuzhang.yaoqingma = invite_code
|
||||||
|
zuzhang.save(update_fields=['yaoqingma'])
|
||||||
|
|
||||||
|
# 4. 获取微信 access_token(按俱乐部对应小程序)
|
||||||
|
access_token = get_club_mini_access_token(club_id, force_refresh=False)
|
||||||
|
if not access_token:
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': '获取微信access_token失败',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
# ===== 修改点:使用 A 接口(getwxacode) =====
|
||||||
|
encoded_invite = urllib.parse.quote(invite_code, safe='')
|
||||||
|
# 新小程序:打手端 Tab「我的」管事注册;旧码 guanshiduan 由小程序兼容页转发
|
||||||
|
page_path = f'pages/fighter/fighter?registerType=guanshi&inviteCode={encoded_invite}'
|
||||||
|
|
||||||
|
wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token)
|
||||||
|
if wx_error and is_weixin_token_invalid(wx_error.get('errcode'), wx_error.get('errmsg', '')):
|
||||||
|
logger.warning('组长海报二维码 token 失效,强制刷新后重试 club=%s', club_id)
|
||||||
|
access_token = get_club_mini_access_token(club_id, force_refresh=True)
|
||||||
|
if not access_token:
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': '获取微信access_token失败',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token)
|
||||||
|
|
||||||
|
if wx_error:
|
||||||
|
errmsg = wx_error.get('errmsg', '未知错误')
|
||||||
|
logger.error(f"微信接口返回错误: {errmsg}")
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': f'生成二维码失败: {errmsg}',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
if wx_resp is None:
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': '调用微信服务失败',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
# 5. 上传到腾讯云 COS
|
||||||
|
timestamp = int(time.time())
|
||||||
|
filename = f"{current_user.UserUID}_{timestamp}.png"
|
||||||
|
oss_path = f"erweima/zuzhang/{club_id}/{filename}"
|
||||||
|
file_obj = io.BytesIO(wx_resp.content)
|
||||||
|
|
||||||
|
try:
|
||||||
|
full_url = upload_to_oss(file_obj, oss_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"上传到COS失败: {e}")
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': '图片上传失败',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
if not full_url:
|
||||||
|
return Response({
|
||||||
|
'code': 500,
|
||||||
|
'message': '图片上传失败',
|
||||||
|
'data': None
|
||||||
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||||
|
|
||||||
|
# 6. 获取相对路径
|
||||||
|
cos_domain = getattr(settings, 'COS_DOMAIN', '')
|
||||||
|
if cos_domain and full_url.startswith(cos_domain):
|
||||||
|
relative_path = full_url[len(cos_domain):].lstrip('/')
|
||||||
|
else:
|
||||||
|
relative_path = oss_path
|
||||||
|
|
||||||
|
# 7. 删除旧二维码文件
|
||||||
|
old_url = zuzhang.haibao_url
|
||||||
|
if old_url:
|
||||||
|
delete_from_oss(old_url)
|
||||||
|
|
||||||
|
# 8. 更新数据库中的 haibao_url 字段
|
||||||
|
with transaction.atomic():
|
||||||
|
zuzhang.haibao_url = relative_path
|
||||||
|
zuzhang.save(update_fields=['haibao_url'])
|
||||||
|
|
||||||
|
# 9. 返回邀请码和二维码相对路径
|
||||||
|
return Response({
|
||||||
|
'code': 0,
|
||||||
|
'message': 'success',
|
||||||
|
'data': {
|
||||||
|
'url': relative_path,
|
||||||
|
'yaoqingma': invite_code
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
def _request_wx_qrcode(self, page_path, access_token):
|
||||||
|
"""
|
||||||
|
调用 getwxacode 生成小程序码。
|
||||||
|
返回 (response, error_dict);成功时 error_dict 为 None。
|
||||||
|
"""
|
||||||
|
wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
|
||||||
|
post_data = {
|
||||||
|
'path': page_path,
|
||||||
|
'width': 430,
|
||||||
|
'auto_color': False,
|
||||||
|
'line_color': {'r': 0, 'g': 0, 'b': 0},
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
wx_resp = requests.post(wx_url, json=post_data, timeout=10)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'请求微信接口失败: {e}')
|
||||||
|
return None, {'errmsg': '调用微信服务失败'}
|
||||||
|
|
||||||
|
if wx_resp.status_code != 200:
|
||||||
|
error_info = {}
|
||||||
|
if wx_resp.headers.get('content-type', '').startswith('application/json'):
|
||||||
|
try:
|
||||||
|
error_info = wx_resp.json()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return wx_resp, {
|
||||||
|
'errcode': error_info.get('errcode'),
|
||||||
|
'errmsg': error_info.get('errmsg', f'HTTP {wx_resp.status_code}'),
|
||||||
|
}
|
||||||
|
|
||||||
|
content_type = wx_resp.headers.get('content-type', '')
|
||||||
|
if 'image' not in content_type:
|
||||||
|
try:
|
||||||
|
error_info = wx_resp.json()
|
||||||
|
return wx_resp, {
|
||||||
|
'errcode': error_info.get('errcode'),
|
||||||
|
'errmsg': error_info.get('errmsg', '未知错误'),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
return wx_resp, {'errmsg': '微信接口返回未知内容'}
|
||||||
|
|
||||||
|
return wx_resp, None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
365
config/views/scripts.py
Normal file
365
config/views/scripts.py
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import secrets
|
||||||
|
import hashlib
|
||||||
|
import threading
|
||||||
|
import urllib.parse
|
||||||
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import transaction, connection
|
||||||
|
from django.db.models import Q, F, Max, Prefetch
|
||||||
|
from gvsdsdk.fluent import db, func, FQ
|
||||||
|
from django.core.cache import cache
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status, permissions
|
||||||
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||||
|
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
|
||||||
|
|
||||||
|
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
|
||||||
|
from utils.weixin_broadcast import WeixinBroadcastSender
|
||||||
|
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||||||
|
# from utils.ip_security import *
|
||||||
|
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
|
||||||
|
|
||||||
|
from backend.utils import update_shangjia_daily
|
||||||
|
from utils.chat_utils import subscribe_merchant_link_chat
|
||||||
|
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
|
||||||
|
|
||||||
|
from ..models import (
|
||||||
|
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
|
||||||
|
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
|
||||||
|
MiniappScriptScene, MiniappScriptAutoReply,
|
||||||
|
)
|
||||||
|
|
||||||
|
from users.models import (
|
||||||
|
AdminProfile, UserDashou, UserBoss,
|
||||||
|
UserShangjia, UserGuanshi, UserZuzhang
|
||||||
|
)
|
||||||
|
from users.business_models import User
|
||||||
|
|
||||||
|
from orders.models import CommissionRate, Order, MerchantOrderExt
|
||||||
|
from orders.notice_tasks import dingdan_guangbo
|
||||||
|
|
||||||
|
from rank.models import Chenghao, DingdanBiaoqian
|
||||||
|
|
||||||
|
from products.models import ShangpinLeixing, Huiyuangoumai
|
||||||
|
|
||||||
|
from ..serializers import PopupConfigSerializer
|
||||||
|
import traceback
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 小程序客服话术 / 自动回复默认值(原位于 miniapp_config.py,按功能组归类到话术模块)
|
||||||
|
MINIAPP_SCRIPT_DEFAULTS = {
|
||||||
|
'cs_welcome': {
|
||||||
|
'title': '',
|
||||||
|
'content': '你好,请问有什么可以帮到您的?',
|
||||||
|
'confirm_text': '',
|
||||||
|
'cancel_text': '',
|
||||||
|
},
|
||||||
|
'chat_image_confirm': {
|
||||||
|
'title': '温馨提示',
|
||||||
|
'content': '平台禁止任何违法违规行为,聊天内容仅限于商家与接单员正常业务对接。严禁赌博、诈骗及其他非法行为,违者后果自负。',
|
||||||
|
'confirm_text': '确认',
|
||||||
|
'cancel_text': '取消',
|
||||||
|
},
|
||||||
|
'merchant_dispatch_confirm': {
|
||||||
|
'title': '确认派单',
|
||||||
|
'content': '确认向该打手派发此订单?',
|
||||||
|
'confirm_text': '确认',
|
||||||
|
'cancel_text': '取消',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _resolve_script_club_id(request):
|
||||||
|
from jituan.services.club_context import resolve_club_id_from_request
|
||||||
|
from jituan.services.club_user import get_user_club_id
|
||||||
|
user = getattr(request, 'user', None)
|
||||||
|
club_id = get_user_club_id(user) if user and getattr(user, 'is_authenticated', False) else None
|
||||||
|
if not club_id:
|
||||||
|
club_id = resolve_club_id_from_request(request)
|
||||||
|
return club_id or 'xq'
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_script_scene(scene_key, row):
|
||||||
|
base = dict(MINIAPP_SCRIPT_DEFAULTS.get(scene_key) or {})
|
||||||
|
if not row:
|
||||||
|
return base
|
||||||
|
if row.title:
|
||||||
|
base['title'] = row.title
|
||||||
|
if row.content:
|
||||||
|
base['content'] = row.content
|
||||||
|
if row.confirm_text:
|
||||||
|
base['confirm_text'] = row.confirm_text
|
||||||
|
if row.cancel_text:
|
||||||
|
base['cancel_text'] = row.cancel_text
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
class HuashuHqView(APIView):
|
||||||
|
"""POST /peizhi/huashuhq 批量获取场景话术"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
scene_keys = request.data.get('scene_keys') or []
|
||||||
|
if not isinstance(scene_keys, list) or not scene_keys:
|
||||||
|
return Response({'code': 400, 'msg': 'scene_keys 不能为空', 'data': None}, status=400)
|
||||||
|
club_id = _resolve_script_club_id(request)
|
||||||
|
rows = MiniappScriptScene.query.filter(
|
||||||
|
club_id=club_id, scene_key__in=scene_keys, enabled=True,
|
||||||
|
)
|
||||||
|
row_map = {r.scene_key: r for r in rows}
|
||||||
|
data = {}
|
||||||
|
for key in scene_keys:
|
||||||
|
if not isinstance(key, str) or not key.strip():
|
||||||
|
continue
|
||||||
|
data[key.strip()] = _merge_script_scene(key.strip(), row_map.get(key.strip()))
|
||||||
|
return Response({'code': 200, 'msg': 'success', 'data': data})
|
||||||
|
|
||||||
|
|
||||||
|
class HuashuMatchView(APIView):
|
||||||
|
"""POST /peizhi/huashu_match 客服关键词自动回复匹配"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
scene_key = (request.data.get('scene_key') or '').strip()
|
||||||
|
user_text = (request.data.get('user_text') or '').strip()
|
||||||
|
if scene_key != 'cs_auto_reply':
|
||||||
|
return Response({'code': 400, 'msg': '不支持的 scene_key', 'data': {'matched': False, 'content': ''}})
|
||||||
|
if not user_text:
|
||||||
|
return Response({'code': 200, 'msg': 'success', 'data': {'matched': False, 'content': ''}})
|
||||||
|
club_id = _resolve_script_club_id(request)
|
||||||
|
rules = MiniappScriptAutoReply.query.filter(
|
||||||
|
club_id=club_id, enabled=True,
|
||||||
|
).order_by('-sort_order', 'id')
|
||||||
|
text_lower = user_text.lower()
|
||||||
|
for rule in rules:
|
||||||
|
kw_raw = (rule.keyword or '').strip()
|
||||||
|
if not kw_raw:
|
||||||
|
continue
|
||||||
|
keywords = [k.strip() for k in kw_raw.split(',') if k.strip()]
|
||||||
|
for kw in keywords:
|
||||||
|
if kw.lower() in text_lower or kw in user_text:
|
||||||
|
reply_type = rule.reply_type or MiniappScriptAutoReply.REPLY_TEXT
|
||||||
|
return Response({
|
||||||
|
'code': 200,
|
||||||
|
'msg': 'success',
|
||||||
|
'data': {
|
||||||
|
'matched': True,
|
||||||
|
'content': rule.reply_content or '',
|
||||||
|
'reply_type': reply_type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return Response({'code': 200, 'msg': 'success', 'data': {'matched': False, 'content': ''}})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 后台:客服话术配置(/houtai/hthqhs、/houtai/htxghs) ----------
|
||||||
|
|
||||||
|
SCRIPT_CONFIG_PERMS = ('kfhs666', '8080a')
|
||||||
|
SCRIPT_SCENE_KEYS = ('chat_image_confirm', 'cs_welcome', 'merchant_dispatch_confirm')
|
||||||
|
|
||||||
|
|
||||||
|
def _has_script_config_perm(permissions):
|
||||||
|
return any(code in permissions for code in SCRIPT_CONFIG_PERMS)
|
||||||
|
|
||||||
|
|
||||||
|
def _script_scene_item_type(scene_key):
|
||||||
|
return 'text_display' if scene_key == 'cs_welcome' else 'confirm_modal'
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_script_scene(row):
|
||||||
|
return {
|
||||||
|
'id': row.id,
|
||||||
|
'scene_key': row.scene_key,
|
||||||
|
'item_type': _script_scene_item_type(row.scene_key),
|
||||||
|
'title': row.title or '',
|
||||||
|
'content': row.content or '',
|
||||||
|
'confirm_text': row.confirm_text or '',
|
||||||
|
'cancel_text': row.cancel_text or '',
|
||||||
|
'is_active': bool(row.enabled),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_script_auto_reply(row):
|
||||||
|
keywords = [k.strip() for k in (row.keyword or '').split(',') if k.strip()]
|
||||||
|
return {
|
||||||
|
'id': row.id,
|
||||||
|
'scene_key': 'cs_auto_reply',
|
||||||
|
'item_type': 'auto_reply',
|
||||||
|
'title': '',
|
||||||
|
'keywords': keywords,
|
||||||
|
'match_mode': 'contains',
|
||||||
|
'priority': row.sort_order or 0,
|
||||||
|
'content': row.reply_content or '',
|
||||||
|
'is_active': bool(row.enabled),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MiniappScriptHoutaiListView(APIView):
|
||||||
|
"""POST /houtai/hthqhs 后台获取话术与自动回复配置"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
from backend.utils import verify_kefu_permission
|
||||||
|
|
||||||
|
username = request.data.get('username')
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||||||
|
if not _has_script_config_perm(permissions):
|
||||||
|
return Response({'code': 403, 'msg': '您没有权限访问客服聊天配置'})
|
||||||
|
|
||||||
|
club_id = _resolve_script_club_id(request)
|
||||||
|
scenes = MiniappScriptScene.query.filter(
|
||||||
|
club_id=club_id, scene_key__in=SCRIPT_SCENE_KEYS,
|
||||||
|
).order_by('scene_key')
|
||||||
|
auto_rules = MiniappScriptAutoReply.query.filter(club_id=club_id).order_by('-sort_order', 'id')
|
||||||
|
|
||||||
|
items = [_serialize_script_scene(r) for r in scenes]
|
||||||
|
items.extend(_serialize_script_auto_reply(r) for r in auto_rules)
|
||||||
|
return Response({'code': 0, 'data': {'items': items, 'club_id': club_id}})
|
||||||
|
|
||||||
|
|
||||||
|
class MiniappScriptHoutaiModifyView(APIView):
|
||||||
|
"""POST /houtai/htxghs 后台增删改话术与自动回复"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
from backend.utils import verify_kefu_permission
|
||||||
|
from jituan.services.display_config import forbid_display_write_in_all_scope
|
||||||
|
|
||||||
|
username = request.data.get('username')
|
||||||
|
kefu, permissions = verify_kefu_permission(request, username)
|
||||||
|
if kefu is None:
|
||||||
|
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
|
||||||
|
if not _has_script_config_perm(permissions):
|
||||||
|
return Response({'code': 403, 'msg': '您没有权限操作客服聊天配置'})
|
||||||
|
|
||||||
|
action = (request.data.get('action') or '').strip()
|
||||||
|
if not action:
|
||||||
|
return Response({'code': 400, 'msg': '缺少 action 参数'})
|
||||||
|
|
||||||
|
club_id = _resolve_script_club_id(request)
|
||||||
|
|
||||||
|
if action == 'delete':
|
||||||
|
return self._delete_item(request, club_id)
|
||||||
|
|
||||||
|
deny = forbid_display_write_in_all_scope(request)
|
||||||
|
if deny:
|
||||||
|
return deny
|
||||||
|
|
||||||
|
if action in ('create', 'update'):
|
||||||
|
item_type = (request.data.get('item_type') or '').strip()
|
||||||
|
if item_type == 'auto_reply':
|
||||||
|
return self._save_auto_reply(request, club_id, action)
|
||||||
|
return self._save_scene(request, club_id, action)
|
||||||
|
|
||||||
|
return Response({'code': 400, 'msg': f'未知的 action: {action}'})
|
||||||
|
|
||||||
|
def _save_scene(self, request, club_id, action):
|
||||||
|
scene_key = (request.data.get('scene_key') or '').strip()
|
||||||
|
if scene_key not in SCRIPT_SCENE_KEYS:
|
||||||
|
return Response({'code': 400, 'msg': '不支持的 scene_key'})
|
||||||
|
|
||||||
|
content = (request.data.get('content') or '').strip()
|
||||||
|
if not content:
|
||||||
|
return Response({'code': 400, 'msg': '请填写正文内容'})
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'title': (request.data.get('title') or '').strip(),
|
||||||
|
'content': content,
|
||||||
|
'confirm_text': (request.data.get('confirm_text') or '').strip(),
|
||||||
|
'cancel_text': (request.data.get('cancel_text') or '').strip(),
|
||||||
|
'enabled': bool(request.data.get('is_active', True)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == 'update':
|
||||||
|
row_id = request.data.get('id')
|
||||||
|
if not row_id:
|
||||||
|
return Response({'code': 400, 'msg': '缺少 id'})
|
||||||
|
row = MiniappScriptScene.query.filter(id=row_id, club_id=club_id).first()
|
||||||
|
if not row:
|
||||||
|
return Response({'code': 404, 'msg': '配置不存在'})
|
||||||
|
for k, v in fields.items():
|
||||||
|
setattr(row, k, v)
|
||||||
|
row.save()
|
||||||
|
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_scene(row)})
|
||||||
|
|
||||||
|
existing = MiniappScriptScene.query.filter(club_id=club_id, scene_key=scene_key).first()
|
||||||
|
if existing:
|
||||||
|
for k, v in fields.items():
|
||||||
|
setattr(existing, k, v)
|
||||||
|
existing.save()
|
||||||
|
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_scene(existing)})
|
||||||
|
|
||||||
|
row = MiniappScriptScene.query.create(club_id=club_id, scene_key=scene_key, **fields)
|
||||||
|
return Response({'code': 0, 'msg': '创建成功', 'data': _serialize_script_scene(row)})
|
||||||
|
|
||||||
|
def _save_auto_reply(self, request, club_id, action):
|
||||||
|
keywords = request.data.get('keywords') or []
|
||||||
|
if not isinstance(keywords, list) or not keywords:
|
||||||
|
return Response({'code': 400, 'msg': '请至少添加一个触发关键词'})
|
||||||
|
|
||||||
|
kw_list = [str(k).strip() for k in keywords if str(k).strip()]
|
||||||
|
if not kw_list:
|
||||||
|
return Response({'code': 400, 'msg': '请至少添加一个触发关键词'})
|
||||||
|
|
||||||
|
content = (request.data.get('content') or '').strip()
|
||||||
|
if not content:
|
||||||
|
return Response({'code': 400, 'msg': '请填写回复内容'})
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'keyword': ','.join(kw_list),
|
||||||
|
'reply_content': content,
|
||||||
|
'sort_order': int(request.data.get('priority') or 0),
|
||||||
|
'enabled': bool(request.data.get('is_active', True)),
|
||||||
|
'reply_type': MiniappScriptAutoReply.REPLY_TEXT,
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == 'update':
|
||||||
|
row_id = request.data.get('id')
|
||||||
|
if not row_id:
|
||||||
|
return Response({'code': 400, 'msg': '缺少 id'})
|
||||||
|
row = MiniappScriptAutoReply.query.filter(id=row_id, club_id=club_id).first()
|
||||||
|
if not row:
|
||||||
|
return Response({'code': 404, 'msg': '规则不存在'})
|
||||||
|
for k, v in fields.items():
|
||||||
|
setattr(row, k, v)
|
||||||
|
row.save()
|
||||||
|
return Response({'code': 0, 'msg': '保存成功', 'data': _serialize_script_auto_reply(row)})
|
||||||
|
|
||||||
|
row = MiniappScriptAutoReply.query.create(club_id=club_id, **fields)
|
||||||
|
return Response({'code': 0, 'msg': '创建成功', 'data': _serialize_script_auto_reply(row)})
|
||||||
|
|
||||||
|
def _delete_item(self, request, club_id):
|
||||||
|
from jituan.services.display_config import forbid_display_write_in_all_scope
|
||||||
|
|
||||||
|
deny = forbid_display_write_in_all_scope(request)
|
||||||
|
if deny:
|
||||||
|
return deny
|
||||||
|
|
||||||
|
row_id = request.data.get('id')
|
||||||
|
if not row_id:
|
||||||
|
return Response({'code': 400, 'msg': '缺少 id'})
|
||||||
|
|
||||||
|
row = MiniappScriptAutoReply.query.filter(id=row_id, club_id=club_id).first()
|
||||||
|
if row:
|
||||||
|
row.delete()
|
||||||
|
return Response({'code': 0, 'msg': '删除成功'})
|
||||||
|
|
||||||
|
row = MiniappScriptScene.query.filter(id=row_id, club_id=club_id).first()
|
||||||
|
if row:
|
||||||
|
return Response({'code': 400, 'msg': '场景话术不支持删除,请改为禁用'})
|
||||||
|
|
||||||
|
return Response({'code': 404, 'msg': '记录不存在'})
|
||||||
82
create_rbac_tables.sql
Normal file
82
create_rbac_tables.sql
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- RBAC 表创建脚本
|
||||||
|
-- 仅创建生产数据库中缺失的表,已存在的表会被跳过
|
||||||
|
-- 执行方式: mysql -u root -p xaio_cheng_xu < create_rbac_tables.sql
|
||||||
|
-- 注意: 索引和外键单独处理,避免重复创建报错
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Part 1: backend RBAC 表(小写表名,verify_kefu_permission 使用)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `role` (
|
||||||
|
`role_id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
|
||||||
|
`role_code` varchar(50) NOT NULL UNIQUE,
|
||||||
|
`role_name` varchar(50) NOT NULL,
|
||||||
|
`description` varchar(200) NOT NULL,
|
||||||
|
`create_time` datetime(6) NOT NULL,
|
||||||
|
`update_time` datetime(6) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `permission` (
|
||||||
|
`perm_id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
|
||||||
|
`perm_code` varchar(100) NOT NULL UNIQUE,
|
||||||
|
`perm_name` varchar(100) NOT NULL,
|
||||||
|
`description` varchar(200) NOT NULL,
|
||||||
|
`create_time` datetime(6) NOT NULL,
|
||||||
|
`update_time` datetime(6) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `user_role` (
|
||||||
|
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
|
||||||
|
`account_id` varchar(11) NOT NULL,
|
||||||
|
`create_time` datetime(6) NOT NULL,
|
||||||
|
`update_time` datetime(6) NOT NULL,
|
||||||
|
`role_id` integer NOT NULL,
|
||||||
|
UNIQUE KEY `user_role_account_id_role_id_uniq` (`account_id`, `role_id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `role_permission` (
|
||||||
|
`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY,
|
||||||
|
`create_time` datetime(6) NOT NULL,
|
||||||
|
`permission_id` integer NOT NULL,
|
||||||
|
`role_id` integer NOT NULL,
|
||||||
|
UNIQUE KEY `role_permission_role_id_permission_id_uniq` (`role_id`, `permission_id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- backend 表索引(忽略已存在的错误)
|
||||||
|
CREATE INDEX `user_role_account_id_idx` ON `user_role` (`account_id`);
|
||||||
|
|
||||||
|
-- backend 外键约束
|
||||||
|
ALTER TABLE `user_role` ADD CONSTRAINT `user_role_role_fk` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`);
|
||||||
|
ALTER TABLE `role_permission` ADD CONSTRAINT `role_permission_permission_fk` FOREIGN KEY (`permission_id`) REFERENCES `permission` (`perm_id`);
|
||||||
|
ALTER TABLE `role_permission` ADD CONSTRAINT `role_permission_role_fk` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Part 2: gvsdsdk RBAC 表(大写表名,UUID 主键)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `AccountActivation` (`AccountActivationCode` BINARY(6) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `CreateTime` datetime(6) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `EphemeralToken` (`ELTUUID` BINARY(16) NOT NULL PRIMARY KEY, `ELTUserUUID` BINARY(16) NOT NULL, `ELTCreateTime` datetime(6) NOT NULL, `ELTEndTime` datetime(6) NOT NULL, `ELTType` smallint NOT NULL, `ELTAllowIP` json NOT NULL, `ELTStatus` smallint NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `Password` (`UserUUID` BINARY(16) NOT NULL PRIMARY KEY, `Password` varchar(1024) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `Permission` (`PermUUID` BINARY(16) NOT NULL PRIMARY KEY, `TenantUUID` BINARY(16) NOT NULL, `PermName` varchar(128) NOT NULL, `PermCode` varchar(128) NOT NULL, `PermCategory` varchar(64) NOT NULL, `PermStatus` smallint NOT NULL, `PermDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `Position` (`PositionUUID` BINARY(16) NOT NULL PRIMARY KEY, `TenantUUID` BINARY(16) NOT NULL, `PositionName` varchar(128) NOT NULL, `PositionCode` varchar(64) NOT NULL, `PositionLevel` smallint NOT NULL, `PositionStatus` smallint NOT NULL, `PositionDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL, `UpdateTime` datetime(6) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `PositionTemplate` (`PositionTemplateUUID` BINARY(16) NOT NULL PRIMARY KEY, `TenantUUID` BINARY(16) NOT NULL, `TemplateName` varchar(128) NOT NULL, `TemplateCode` varchar(64) NOT NULL, `TemplateStatus` smallint NOT NULL, `TemplateDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL, `UpdateTime` datetime(6) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `PositionTemplatePermission` (`PTPermUUID` BINARY(16) NOT NULL PRIMARY KEY, `PositionTemplateUUID` BINARY(16) NOT NULL, `PermUUID` BINARY(16) NOT NULL, `CreateTime` datetime(6) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `Role` (`RoleUUID` BINARY(16) NOT NULL PRIMARY KEY, `TenantUUID` BINARY(16) NOT NULL, `RoleName` varchar(128) NOT NULL, `RoleType` smallint NOT NULL, `RoleStatus` smallint NOT NULL, `AssignScope` smallint NOT NULL, `AssignScopeUUID` BINARY(16) NULL, `RoleDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `RolePermission` (`RolePermUUID` BINARY(16) NOT NULL PRIMARY KEY, `RoleUUID` BINARY(16) NOT NULL, `PermUUID` BINARY(16) NOT NULL, `CreateTime` datetime(6) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `RoleShop` (`RoleShopUUID` BINARY(16) NOT NULL PRIMARY KEY, `RoleUUID` BINARY(16) NOT NULL, `ShopUUID` BINARY(16) NOT NULL, `IsMainShop` smallint NOT NULL, `CreateTime` datetime(6) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `Shop` (`ShopUUID` BINARY(16) NOT NULL PRIMARY KEY, `CompanyUUID` BINARY(16) NOT NULL, `ShopName` varchar(128) NOT NULL, `ShopCode` varchar(64) NOT NULL, `ShopType` smallint NOT NULL, `ShopStatus` smallint NOT NULL, `ShopAddress` varchar(512) NOT NULL, `ShopContact` varchar(128) NOT NULL, `ShopPhone` varchar(32) NOT NULL, `ShopDesc` varchar(512) NOT NULL, `CreateTime` datetime(6) NOT NULL, `UpdateTime` datetime(6) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `User2FA` (`User2FAUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `SecretKey` varchar(64) NOT NULL, `IsEnabled` smallint NOT NULL, `Verified` smallint NOT NULL, `LastUsedAt` datetime(6) NULL, `CreateTime` datetime(6) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `UserABAC` (`UserABACUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `HierarchicalPermissionIdentifiers` varchar(256) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `UserAvatar` (`UserUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserAvatar` longblob NOT NULL, `AvatarUploadTime` datetime(6) NOT NULL, `AvatarFileType` varchar(16) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `UserDept` (`UserDeptUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `DeptUUID` BINARY(16) NOT NULL, `PositionUUID` BINARY(16) NULL, `IsMainDept` smallint NOT NULL, `CreateTime` datetime(6) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `UserLoginLog` (`LogUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `LoginIP` varchar(39) NOT NULL, `LoginPlatform` varchar(32) NOT NULL, `LoginStatus` smallint NOT NULL, `LoginTime` datetime(6) NOT NULL, `UserAgent` varchar(512) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `UserPending2FA` (`PendingUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL UNIQUE, `SecretKey` varchar(64) NOT NULL, `ExpireAt` datetime(6) NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS `UserRole` (`UserRoleUUID` BINARY(16) NOT NULL PRIMARY KEY, `UserUUID` BINARY(16) NOT NULL, `RoleUUID` BINARY(16) NOT NULL, `CompanyUUID` BINARY(16) NOT NULL, `CreateTime` datetime(6) NOT NULL);
|
||||||
|
|
||||||
|
-- gvsdsdk 索引
|
||||||
|
CREATE INDEX `EphemELTUserUUID_idx` ON `EphemeralToken` (`ELTUserUUID`);
|
||||||
|
CREATE INDEX `EphemELTStatus_idx` ON `EphemeralToken` (`ELTStatus`);
|
||||||
|
CREATE INDEX `User2FA_UserUUID_idx` ON `User2FA` (`UserUUID`);
|
||||||
|
CREATE INDEX `UserLoginLog_UserUUID_idx` ON `UserLoginLog` (`UserUUID`);
|
||||||
1
data/ai_chat/context.json
Normal file
1
data/ai_chat/context.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
73
deploy/pre_migrate_dedupe.sql
Normal file
73
deploy/pre_migrate_dedupe.sql
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
-- 部署 jituan 前若 migrate 报 Duplicate entry 唯一约束冲突时使用。
|
||||||
|
-- 不要用 mysql 命令行(1Panel MySQL 不在本机 socket),请用:
|
||||||
|
-- python manage.py dbshell < deploy/pre_migrate_dedupe.sql
|
||||||
|
-- 或半路失败的 config.0006:
|
||||||
|
-- python manage.py recover_config_0006
|
||||||
|
-- python manage.py migrate config 0006 --fake
|
||||||
|
-- python manage.py migrate
|
||||||
|
|
||||||
|
-- config.0006 tupianpeizhi
|
||||||
|
DELETE FROM tupianpeizhi
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT keep_id FROM (
|
||||||
|
SELECT MAX(id) AS keep_id FROM tupianpeizhi GROUP BY club_id, ImageType
|
||||||
|
) AS t
|
||||||
|
);
|
||||||
|
|
||||||
|
-- config.0006 popup_page
|
||||||
|
DELETE FROM popup_page
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT keep_id FROM (
|
||||||
|
SELECT MAX(id) AS keep_id FROM popup_page GROUP BY club_id, page_key
|
||||||
|
) AS t
|
||||||
|
);
|
||||||
|
|
||||||
|
-- products.0005 duoci_fenhong
|
||||||
|
DELETE FROM duoci_fenhong
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT keep_id FROM (
|
||||||
|
SELECT MAX(id) AS keep_id FROM duoci_fenhong GROUP BY club_id, huiyuan, yonghuid, cishu
|
||||||
|
) AS t
|
||||||
|
);
|
||||||
|
|
||||||
|
-- backend.0003 提现日统计
|
||||||
|
DELETE FROM tixian_ri_tongji
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT keep_id FROM (
|
||||||
|
SELECT MAX(id) AS keep_id FROM tixian_ri_tongji GROUP BY club_id, Date, WithdrawalType
|
||||||
|
) AS t
|
||||||
|
);
|
||||||
|
|
||||||
|
-- backend.0004 角色日统计
|
||||||
|
DELETE FROM shangjia_ri_tongji
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT keep_id FROM (
|
||||||
|
SELECT MAX(id) AS keep_id FROM shangjia_ri_tongji GROUP BY club_id, MerchantID, Date
|
||||||
|
) AS t
|
||||||
|
);
|
||||||
|
DELETE FROM dashou_ri_tongji
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT keep_id FROM (
|
||||||
|
SELECT MAX(id) AS keep_id FROM dashou_ri_tongji GROUP BY club_id, PlayerID, Date
|
||||||
|
) AS t
|
||||||
|
);
|
||||||
|
DELETE FROM zuzhang_ri_tongji
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT keep_id FROM (
|
||||||
|
SELECT MAX(id) AS keep_id FROM zuzhang_ri_tongji GROUP BY club_id, LeaderID, Date
|
||||||
|
) AS t
|
||||||
|
);
|
||||||
|
DELETE FROM guanshi_ri_tongji
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT keep_id FROM (
|
||||||
|
SELECT MAX(id) AS keep_id FROM guanshi_ri_tongji GROUP BY club_id, ManagerID, Date
|
||||||
|
) AS t
|
||||||
|
);
|
||||||
|
DELETE FROM guanshi_xufei_ri_tongji
|
||||||
|
WHERE id NOT IN (
|
||||||
|
SELECT keep_id FROM (
|
||||||
|
SELECT MAX(id) AS keep_id FROM guanshi_xufei_ri_tongji GROUP BY club_id, ManagerID, Date
|
||||||
|
) AS t
|
||||||
|
);
|
||||||
|
|
||||||
|
-- szjilu 应只有 1 行;多行时勿自动删,需人工核对 TotalIncome 后处理
|
||||||
163
find.py
163
find.py
@@ -1,163 +0,0 @@
|
|||||||
"""
|
|
||||||
REPL 模式文件内容搜索工具
|
|
||||||
支持正则或纯文本搜索,默认搜索当前目录
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# 硬编码排除目录
|
|
||||||
EXCLUDE_DIRS = {
|
|
||||||
'.git', '.vs', 'node_modules', '__pycache__', '.idea',
|
|
||||||
'miniprogram_npm', '_backup_pre_redesign',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 排除的文件扩展名
|
|
||||||
EXCLUDE_EXTS = {
|
|
||||||
'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.bmp',
|
|
||||||
'.mp3', '.mp4', '.wav', '.avi',
|
|
||||||
'.zip', '.rar', '.7z', '.tar', '.gz',
|
|
||||||
'.exe', '.dll', '.so', '.dylib',
|
|
||||||
'.pyc', '.pyo', '.class', '.o', '.obj',
|
|
||||||
'.db', '.sqlite', '.vsidx',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 默认搜索根目录
|
|
||||||
DEFAULT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
|
|
||||||
|
|
||||||
def should_skip(dirpath):
|
|
||||||
"""判断目录是否应跳过"""
|
|
||||||
parts = dirpath.replace('\\', '/').split('/')
|
|
||||||
return any(p in EXCLUDE_DIRS for p in parts)
|
|
||||||
|
|
||||||
|
|
||||||
def search(pattern, root=DEFAULT_ROOT, use_regex=True, max_results=50):
|
|
||||||
"""搜索文件内容"""
|
|
||||||
try:
|
|
||||||
if use_regex:
|
|
||||||
compiled = re.compile(pattern, re.IGNORECASE)
|
|
||||||
except re.error as e:
|
|
||||||
print(f' 正则语法错误: {e}')
|
|
||||||
return
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
for dirpath, dirnames, filenames in os.walk(root):
|
|
||||||
# 就地修改 dirnames 以跳过排除目录
|
|
||||||
dirnames[:] = [d for d in dirnames if d not in EXCLUDE_DIRS]
|
|
||||||
|
|
||||||
for fname in filenames:
|
|
||||||
ext = os.path.splitext(fname)[1].lower()
|
|
||||||
if ext in EXCLUDE_EXTS:
|
|
||||||
continue
|
|
||||||
|
|
||||||
fpath = os.path.join(dirpath, fname)
|
|
||||||
relpath = os.path.relpath(fpath, root)
|
|
||||||
try:
|
|
||||||
with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
|
|
||||||
for lineno, line in enumerate(f, 1):
|
|
||||||
line_stripped = line.rstrip('\n\r')
|
|
||||||
if use_regex:
|
|
||||||
match = compiled.search(line_stripped)
|
|
||||||
if match:
|
|
||||||
highlight = line_stripped[:match.start()] + \
|
|
||||||
f'>>> {match.group()} <<<' + \
|
|
||||||
line_stripped[match.end():]
|
|
||||||
print(f' {relpath}:{lineno} {highlight}')
|
|
||||||
count += 1
|
|
||||||
else:
|
|
||||||
if pattern.lower() in line_stripped.lower():
|
|
||||||
print(f' {relpath}:{lineno} {line_stripped}')
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
if count >= max_results:
|
|
||||||
print(f'\n ... 已达上限 {max_results} 条,停止')
|
|
||||||
return
|
|
||||||
except (PermissionError, OSError):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if count == 0:
|
|
||||||
print(' 未找到匹配内容')
|
|
||||||
else:
|
|
||||||
print(f'\n 共 {count} 条匹配')
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print('=== 文件内容搜索工具 ===')
|
|
||||||
print(f'搜索根目录: {DEFAULT_ROOT}')
|
|
||||||
print(f'排除目录: {EXCLUDE_DIRS}')
|
|
||||||
print()
|
|
||||||
print('命令:')
|
|
||||||
print(' /regex <pattern> 正则搜索')
|
|
||||||
print(' /text <text> 纯文本搜索')
|
|
||||||
print(' /max <n> 设置结果上限(默认50)')
|
|
||||||
print(' /dir <path> 切换搜索目录')
|
|
||||||
print(' /help 显示帮助')
|
|
||||||
print(' /quit 退出')
|
|
||||||
print()
|
|
||||||
print('直接输入内容默认使用正则搜索')
|
|
||||||
print()
|
|
||||||
|
|
||||||
root = DEFAULT_ROOT
|
|
||||||
use_regex = True
|
|
||||||
max_results = 50
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
raw = input('find> ').strip()
|
|
||||||
except (EOFError, KeyboardInterrupt):
|
|
||||||
print('\n再见')
|
|
||||||
break
|
|
||||||
|
|
||||||
if not raw:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if raw == '/quit':
|
|
||||||
print('再见')
|
|
||||||
break
|
|
||||||
|
|
||||||
if raw == '/help':
|
|
||||||
print(' /regex <pattern> 正则搜索')
|
|
||||||
print(' /text <text> 纯文本搜索')
|
|
||||||
print(' /max <n> 设置结果上限')
|
|
||||||
print(' /dir <path> 切换搜索目录')
|
|
||||||
print(' /help 显示帮助')
|
|
||||||
print(' /quit 退出')
|
|
||||||
print(' 直接输入内容默认使用正则搜索')
|
|
||||||
continue
|
|
||||||
|
|
||||||
if raw.startswith('/regex '):
|
|
||||||
pattern = raw[7:].strip()
|
|
||||||
if pattern:
|
|
||||||
search(pattern, root, use_regex=True, max_results=max_results)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if raw.startswith('/text '):
|
|
||||||
pattern = raw[6:].strip()
|
|
||||||
if pattern:
|
|
||||||
search(pattern, root, use_regex=False, max_results=max_results)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if raw.startswith('/max '):
|
|
||||||
try:
|
|
||||||
max_results = int(raw[5:].strip())
|
|
||||||
print(f' 结果上限设为 {max_results}')
|
|
||||||
except ValueError:
|
|
||||||
print(' 请输入数字')
|
|
||||||
continue
|
|
||||||
|
|
||||||
if raw.startswith('/dir '):
|
|
||||||
new_dir = raw[5:].strip()
|
|
||||||
if os.path.isdir(new_dir):
|
|
||||||
root = os.path.abspath(new_dir)
|
|
||||||
print(f' 搜索目录切换为: {root}')
|
|
||||||
else:
|
|
||||||
print(f' 目录不存在: {new_dir}')
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 默认正则搜索
|
|
||||||
search(raw, root, use_regex=use_regex, max_results=max_results)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
"""
|
|
||||||
修复 django_migrations 表中的旧 app 名称引用。
|
|
||||||
在服务器上执行: python manage.py shell < fix_migration_history.py
|
|
||||||
或: python manage.py shell
|
|
||||||
然后粘贴此脚本内容
|
|
||||||
"""
|
|
||||||
from django.db import connection
|
|
||||||
|
|
||||||
APP_NAME_MAPPINGS = {
|
|
||||||
'houtai': 'backend',
|
|
||||||
'yonghu': 'users',
|
|
||||||
'dingdan': 'orders',
|
|
||||||
'peizhi': 'config',
|
|
||||||
'shangpin': 'products',
|
|
||||||
'shangdian': 'shop',
|
|
||||||
'dengji': 'rank',
|
|
||||||
}
|
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
|
||||||
for old_name, new_name in APP_NAME_MAPPINGS.items():
|
|
||||||
cursor.execute(
|
|
||||||
"SELECT COUNT(*) FROM django_migrations WHERE app = %s",
|
|
||||||
[old_name]
|
|
||||||
)
|
|
||||||
count = cursor.fetchone()[0]
|
|
||||||
if count > 0:
|
|
||||||
cursor.execute(
|
|
||||||
"UPDATE django_migrations SET app = %s WHERE app = %s",
|
|
||||||
[new_name, old_name]
|
|
||||||
)
|
|
||||||
print(f"已更新 {count} 条记录: {old_name} -> {new_name}")
|
|
||||||
else:
|
|
||||||
print(f"无需更新: {old_name} (无记录)")
|
|
||||||
|
|
||||||
print("\n修复完成!请运行 python manage.py migrate")
|
|
||||||
@@ -13,7 +13,8 @@ class ELTAuthentication:
|
|||||||
except (ValueError, AttributeError):
|
except (ValueError, AttributeError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
from gvsdsdk.models import User, EphemeralToken
|
from gvsdsdk.models import EphemeralToken
|
||||||
|
from gvsds.apps.core.models import User
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
54
gvsdsdk/auth/jwt_auth.py
Normal file
54
gvsdsdk/auth/jwt_auth.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""自定义 JWT 认证 — 向后兼容旧 token 的 user_id claim"""
|
||||||
|
|
||||||
|
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||||
|
from rest_framework_simplejwt.exceptions import InvalidToken, AuthenticationFailed
|
||||||
|
from rest_framework_simplejwt.settings import api_settings
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class CompatibleJWTAuthentication(JWTAuthentication):
|
||||||
|
"""
|
||||||
|
兼容旧 token 的 JWT 认证类。
|
||||||
|
|
||||||
|
旧 token(USER_ID_CLAIM='user_id' 时生成)payload 中使用 'user_id' claim,
|
||||||
|
值为 user.id(UUID 字符串)。
|
||||||
|
新 token(USER_ID_CLAIM='UserUID' 时生成)payload 中使用 'UserUID' claim,
|
||||||
|
值为 user.UserUID(7位业务ID)。
|
||||||
|
|
||||||
|
此类先尝试新 claim,找不到时回退到旧 claim,并用对应字段查库。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_user(self, validated_token):
|
||||||
|
# 1. 先尝试当前配置的 USER_ID_CLAIM
|
||||||
|
user_id = validated_token.get(api_settings.USER_ID_CLAIM)
|
||||||
|
lookup_field = api_settings.USER_ID_FIELD
|
||||||
|
|
||||||
|
if user_id is None:
|
||||||
|
# 2. 回退:尝试旧 token 中的 'user_id' claim
|
||||||
|
user_id = validated_token.get('user_id')
|
||||||
|
if user_id is None:
|
||||||
|
raise InvalidToken(
|
||||||
|
{'detail': _('令牌未包含用户标识符'), 'code': 'token_not_valid'}
|
||||||
|
)
|
||||||
|
# 旧 token 的 user_id 来自 user.id(即 UserUUID),
|
||||||
|
# 需要用 UserUUID 字段查库
|
||||||
|
lookup_field = 'UserUUID'
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = self.user_model.objects.get(**{lookup_field: user_id})
|
||||||
|
except self.user_model.DoesNotExist:
|
||||||
|
raise AuthenticationFailed(_('用户不存在'), code='user_not_found')
|
||||||
|
|
||||||
|
if not user.is_active:
|
||||||
|
raise AuthenticationFailed(_('用户已被禁用'), code='user_inactive')
|
||||||
|
|
||||||
|
if api_settings.CHECK_REVOKE_TOKEN:
|
||||||
|
from rest_framework_simplejwt.utils import get_md5_hash_password
|
||||||
|
if validated_token.get(
|
||||||
|
api_settings.REVOKE_TOKEN_CLAIM
|
||||||
|
) != get_md5_hash_password(user.password):
|
||||||
|
raise AuthenticationFailed(
|
||||||
|
_('用户密码已更改'), code='password_changed'
|
||||||
|
)
|
||||||
|
|
||||||
|
return user
|
||||||
@@ -87,32 +87,39 @@ class RoleManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def GetUsersForRole(RoleUUID):
|
def GetUsersForRole(RoleUUID):
|
||||||
"""获取拥有指定角色的所有用户"""
|
"""获取拥有指定角色的所有用户"""
|
||||||
from gvsdsdk.models import User
|
from gvsds.apps.core.models import User
|
||||||
user_uuids = UserRole.objects.filter(RoleUUID=RoleUUID).values_list('UserUUID', flat=True)
|
user_uuids = UserRole.objects.filter(RoleUUID=RoleUUID).values_list('UserUUID', flat=True)
|
||||||
return User.objects.filter(UserUUID__in=user_uuids)
|
return User.objects.filter(UserUUID__in=user_uuids)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def AssignPermission(RoleUUID, PermUUID):
|
def AssignPermission(RoleUUID, PermUUID, CompanyUUID):
|
||||||
"""为角色分配权限"""
|
"""为角色分配权限(指定公司)"""
|
||||||
rp = RolePermission(
|
rp = RolePermission(
|
||||||
RolePermUUID=uuid.uuid4().bytes,
|
RolePermUUID=uuid.uuid4().bytes,
|
||||||
RoleUUID=RoleUUID,
|
RoleUUID=RoleUUID,
|
||||||
PermUUID=PermUUID,
|
PermUUID=PermUUID,
|
||||||
|
CompanyUUID=CompanyUUID,
|
||||||
CreateTime=datetime.datetime.utcnow(),
|
CreateTime=datetime.datetime.utcnow(),
|
||||||
)
|
)
|
||||||
rp.save()
|
rp.save()
|
||||||
return rp
|
return rp
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def RemovePermission(RoleUUID, PermUUID):
|
def RemovePermission(RoleUUID, PermUUID, CompanyUUID=None):
|
||||||
"""移除角色的指定权限"""
|
"""移除角色的指定权限"""
|
||||||
deleted, _ = RolePermission.objects.filter(RoleUUID=RoleUUID, PermUUID=PermUUID).delete()
|
qs = RolePermission.objects.filter(RoleUUID=RoleUUID, PermUUID=PermUUID)
|
||||||
|
if CompanyUUID is not None:
|
||||||
|
qs = qs.filter(CompanyUUID=CompanyUUID)
|
||||||
|
deleted, _ = qs.delete()
|
||||||
return deleted > 0
|
return deleted > 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def GetPermissionsForRole(RoleUUID):
|
def GetPermissionsForRole(RoleUUID, CompanyUUID=None):
|
||||||
"""获取角色的所有权限"""
|
"""获取角色的所有权限(可按公司过滤)"""
|
||||||
perm_uuids = RolePermission.objects.filter(RoleUUID=RoleUUID).values_list('PermUUID', flat=True)
|
qs = RolePermission.objects.filter(RoleUUID=RoleUUID)
|
||||||
|
if CompanyUUID is not None:
|
||||||
|
qs = qs.filter(CompanyUUID=CompanyUUID)
|
||||||
|
perm_uuids = qs.values_list('PermUUID', flat=True)
|
||||||
return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1)
|
return Permission.objects.filter(PermUUID__in=perm_uuids, PermStatus=1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from gvsdsdk.models import User, UserRole, Role, RolePermission, Permission, UserABAC
|
from gvsds.apps.core.models import User
|
||||||
|
from gvsdsdk.models import UserRole, Role, RolePermission, Permission, UserABAC
|
||||||
from rest_framework.permissions import BasePermission
|
from rest_framework.permissions import BasePermission
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
|
|||||||
"""权限验证器——混合 ABAC + RBAC 检查
|
"""权限验证器——混合 ABAC + RBAC 检查
|
||||||
|
|
||||||
RBAC 仅检查当前角色(CurrentRoleUUID Cookie 指定的角色)的权限,
|
RBAC 仅检查当前角色(CurrentRoleUUID Cookie 指定的角色)的权限,
|
||||||
通过 RolePermission → Permission 链路查询。
|
通过 RolePermission → Permission 链路查询,按当前公司过滤。
|
||||||
超级管理员/员工直接放行。
|
超级管理员/员工直接放行。
|
||||||
"""
|
"""
|
||||||
if not permission:
|
if not permission:
|
||||||
@@ -63,6 +64,7 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
current_role = getattr(request, 'current_role', None) if request else None
|
current_role = getattr(request, 'current_role', None) if request else None
|
||||||
|
current_company = getattr(request, 'current_company_uuid', None) if request else None
|
||||||
|
|
||||||
for attr in attributes:
|
for attr in attributes:
|
||||||
# ABAC 检查
|
# ABAC 检查
|
||||||
@@ -73,13 +75,16 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
|
|||||||
if abac_match:
|
if abac_match:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# RBAC 检查:仅检查当前角色
|
# RBAC 检查:仅检查当前角色,按公司过滤权限
|
||||||
if current_role and isinstance(current_role, Role):
|
if current_role and isinstance(current_role, Role):
|
||||||
if current_role.RoleStatus != 1:
|
if current_role.RoleStatus != 1:
|
||||||
return False
|
return False
|
||||||
perm_uuids = RolePermission.objects.filter(
|
rp_qs = RolePermission.objects.filter(
|
||||||
RoleUUID=current_role.RoleUUID
|
RoleUUID=current_role.RoleUUID
|
||||||
).values_list('PermUUID', flat=True)
|
)
|
||||||
|
if current_company:
|
||||||
|
rp_qs = rp_qs.filter(CompanyUUID=current_company)
|
||||||
|
perm_uuids = rp_qs.values_list('PermUUID', flat=True)
|
||||||
rbac_result = Permission.objects.filter(
|
rbac_result = Permission.objects.filter(
|
||||||
PermUUID__in=perm_uuids,
|
PermUUID__in=perm_uuids,
|
||||||
PermCode=attr,
|
PermCode=attr,
|
||||||
@@ -94,9 +99,12 @@ def PermissionVerifier(permission, attributes, user=None, request=None):
|
|||||||
RoleUUID__in=user_role_uuids,
|
RoleUUID__in=user_role_uuids,
|
||||||
RoleStatus=1,
|
RoleStatus=1,
|
||||||
).values_list('RoleUUID', flat=True)
|
).values_list('RoleUUID', flat=True)
|
||||||
perm_uuids = RolePermission.objects.filter(
|
rp_qs = RolePermission.objects.filter(
|
||||||
RoleUUID__in=active_role_uuids
|
RoleUUID__in=active_role_uuids
|
||||||
).values_list('PermUUID', flat=True)
|
)
|
||||||
|
if current_company:
|
||||||
|
rp_qs = rp_qs.filter(CompanyUUID=current_company)
|
||||||
|
perm_uuids = rp_qs.values_list('PermUUID', flat=True)
|
||||||
rbac_result = Permission.objects.filter(
|
rbac_result = Permission.objects.filter(
|
||||||
PermUUID__in=perm_uuids,
|
PermUUID__in=perm_uuids,
|
||||||
PermCode=attr,
|
PermCode=attr,
|
||||||
|
|||||||
@@ -1,352 +1,352 @@
|
|||||||
"""模块:分佣 SAAS 数据模型
|
"""模块:分佣 SAAS 数据模型
|
||||||
SAAS 级分佣引擎——事件驱动、规则外置、参与方可配置。
|
SAAS 级分佣引擎——事件驱动、规则外置、参与方可配置。
|
||||||
子服务只管说"发生了什么",SAAS 负责"怎么分"。"""
|
子服务只管说"发生了什么",SAAS 负责"怎么分"。"""
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
import datetime
|
import datetime
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from gvsdsdk.model_base import QModel
|
from gvsdsdk.model_base import QModel
|
||||||
from gvsdsdk.models import UUIDField
|
from gvsdsdk.models import UUIDField
|
||||||
|
|
||||||
|
|
||||||
class CommissionEvent(QModel):
|
class CommissionEvent(QModel):
|
||||||
"""分佣事件表——子服务触发的分佣事件
|
"""分佣事件表——子服务触发的分佣事件
|
||||||
|
|
||||||
子服务在业务发生时(订单完成、会员购买、充值到账等)
|
子服务在业务发生时(订单完成、会员购买、充值到账等)
|
||||||
调用 CommissionEngine.Dispatch() 写入此表。
|
调用 CommissionEngine.Dispatch() 写入此表。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
EVT_ORDER_COMPLETE = 'order_complete'
|
EVT_ORDER_COMPLETE = 'order_complete'
|
||||||
EVT_MEMBER_PURCHASE = 'member_purchase'
|
EVT_MEMBER_PURCHASE = 'member_purchase'
|
||||||
EVT_RECHARGE = 'recharge'
|
EVT_RECHARGE = 'recharge'
|
||||||
EVT_PENALTY = 'penalty'
|
EVT_PENALTY = 'penalty'
|
||||||
EVT_REGISTER = 'register'
|
EVT_REGISTER = 'register'
|
||||||
EVT_CUSTOM = 'custom'
|
EVT_CUSTOM = 'custom'
|
||||||
|
|
||||||
EVT_TYPE_CHOICES = [
|
EVT_TYPE_CHOICES = [
|
||||||
(EVT_ORDER_COMPLETE, '订单完成'),
|
(EVT_ORDER_COMPLETE, '订单完成'),
|
||||||
(EVT_MEMBER_PURCHASE, '会员购买'),
|
(EVT_MEMBER_PURCHASE, '会员购买'),
|
||||||
(EVT_RECHARGE, '充值到账'),
|
(EVT_RECHARGE, '充值到账'),
|
||||||
(EVT_PENALTY, '罚款缴纳'),
|
(EVT_PENALTY, '罚款缴纳'),
|
||||||
(EVT_REGISTER, '用户注册'),
|
(EVT_REGISTER, '用户注册'),
|
||||||
(EVT_CUSTOM, '自定义事件'),
|
(EVT_CUSTOM, '自定义事件'),
|
||||||
]
|
]
|
||||||
|
|
||||||
EVT_PENDING = 0
|
EVT_PENDING = 0
|
||||||
EVT_PROCESSING = 1
|
EVT_PROCESSING = 1
|
||||||
EVT_SETTLED = 2
|
EVT_SETTLED = 2
|
||||||
EVT_FAILED = 3
|
EVT_FAILED = 3
|
||||||
|
|
||||||
EVT_STATUS_CHOICES = [
|
EVT_STATUS_CHOICES = [
|
||||||
(EVT_PENDING, '待处理'),
|
(EVT_PENDING, '待处理'),
|
||||||
(EVT_PROCESSING, '处理中'),
|
(EVT_PROCESSING, '处理中'),
|
||||||
(EVT_SETTLED, '已结算'),
|
(EVT_SETTLED, '已结算'),
|
||||||
(EVT_FAILED, '结算失败'),
|
(EVT_FAILED, '结算失败'),
|
||||||
]
|
]
|
||||||
|
|
||||||
EventUUID = UUIDField(primary_key=True)
|
EventUUID = UUIDField(primary_key=True)
|
||||||
TenantUUID = UUIDField()
|
TenantUUID = UUIDField()
|
||||||
EventType = models.CharField(max_length=32, choices=EVT_TYPE_CHOICES)
|
EventType = models.CharField(max_length=32, choices=EVT_TYPE_CHOICES)
|
||||||
EventKey = models.CharField(max_length=128)
|
EventKey = models.CharField(max_length=128)
|
||||||
TotalAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
|
TotalAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
|
||||||
SourceUserUUID = UUIDField(null=True, blank=True)
|
SourceUserUUID = UUIDField(null=True, blank=True)
|
||||||
EventPayload = models.JSONField(default=dict)
|
EventPayload = models.JSONField(default=dict)
|
||||||
Participants = models.JSONField(default=dict)
|
Participants = models.JSONField(default=dict)
|
||||||
EventStatus = models.SmallIntegerField(default=EVT_PENDING, choices=EVT_STATUS_CHOICES)
|
EventStatus = models.SmallIntegerField(default=EVT_PENDING, choices=EVT_STATUS_CHOICES)
|
||||||
EventTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
EventTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
||||||
ProcessTime = models.DateTimeField(null=True, blank=True)
|
ProcessTime = models.DateTimeField(null=True, blank=True)
|
||||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'CommissionEvent'
|
db_table = 'CommissionEvent'
|
||||||
app_label = 'gvsdsdk'
|
app_label = 'gvsdsdk'
|
||||||
verbose_name = '分佣事件'
|
verbose_name = '分佣事件'
|
||||||
verbose_name_plural = '分佣事件'
|
verbose_name_plural = '分佣事件'
|
||||||
unique_together = [('TenantUUID', 'EventKey')]
|
unique_together = [('TenantUUID', 'EventKey')]
|
||||||
managed = False
|
managed = False
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['TenantUUID', 'EventType', 'EventStatus'], name='idx_cmevt_tenant_type_status'),
|
models.Index(fields=['TenantUUID', 'EventType', 'EventStatus'], name='idx_cmevt_tenant_type_status'),
|
||||||
models.Index(fields=['EventKey'], name='idx_cmevt_event_key'),
|
models.Index(fields=['EventKey'], name='idx_cmevt_event_key'),
|
||||||
models.Index(fields=['CreateTime'], name='idx_cmevt_create_time'),
|
models.Index(fields=['CreateTime'], name='idx_cmevt_CreateTime'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.EventType}:{self.EventKey}'
|
return f'{self.EventType}:{self.EventKey}'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def IsSettled(self):
|
def IsSettled(self):
|
||||||
return self.EventStatus == self.EVT_SETTLED
|
return self.EventStatus == self.EVT_SETTLED
|
||||||
|
|
||||||
def MarkProcessing(self):
|
def MarkProcessing(self):
|
||||||
self.EventStatus = self.EVT_PROCESSING
|
self.EventStatus = self.EVT_PROCESSING
|
||||||
self.save(update_fields=['EventStatus'])
|
self.save(update_fields=['EventStatus'])
|
||||||
|
|
||||||
def MarkSettled(self):
|
def MarkSettled(self):
|
||||||
self.EventStatus = self.EVT_SETTLED
|
self.EventStatus = self.EVT_SETTLED
|
||||||
self.ProcessTime = datetime.datetime.utcnow()
|
self.ProcessTime = datetime.datetime.utcnow()
|
||||||
self.save(update_fields=['EventStatus', 'ProcessTime'])
|
self.save(update_fields=['EventStatus', 'ProcessTime'])
|
||||||
|
|
||||||
def MarkFailed(self):
|
def MarkFailed(self):
|
||||||
self.EventStatus = self.EVT_FAILED
|
self.EventStatus = self.EVT_FAILED
|
||||||
self.ProcessTime = datetime.datetime.utcnow()
|
self.ProcessTime = datetime.datetime.utcnow()
|
||||||
self.save(update_fields=['EventStatus', 'ProcessTime'])
|
self.save(update_fields=['EventStatus', 'ProcessTime'])
|
||||||
|
|
||||||
|
|
||||||
class RateTable(QModel):
|
class RateTable(QModel):
|
||||||
"""费率表——可复用的费率配置
|
"""费率表——可复用的费率配置
|
||||||
|
|
||||||
每个租户可创建多张费率表,不同事件类型绑定不同的表。
|
每个租户可创建多张费率表,不同事件类型绑定不同的表。
|
||||||
例如:打手分成费率表、提现手续费率表、罚款分红费率表。
|
例如:打手分成费率表、提现手续费率表、罚款分红费率表。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RateTableUUID = UUIDField(primary_key=True)
|
RateTableUUID = UUIDField(primary_key=True)
|
||||||
TenantUUID = UUIDField()
|
TenantUUID = UUIDField()
|
||||||
TableCode = models.CharField(max_length=64)
|
TableCode = models.CharField(max_length=64)
|
||||||
TableName = models.CharField(max_length=128)
|
TableName = models.CharField(max_length=128)
|
||||||
TableDescription = models.CharField(max_length=512, default='')
|
TableDescription = models.CharField(max_length=512, default='')
|
||||||
TableStatus = models.SmallIntegerField(default=1)
|
TableStatus = models.SmallIntegerField(default=1)
|
||||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
||||||
UpdateTime = models.DateTimeField(null=True, blank=True)
|
UpdateTime = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'CommissionRateTable'
|
db_table = 'CommissionRateTable'
|
||||||
app_label = 'gvsdsdk'
|
app_label = 'gvsdsdk'
|
||||||
verbose_name = '费率表'
|
verbose_name = '费率表'
|
||||||
verbose_name_plural = '费率表'
|
verbose_name_plural = '费率表'
|
||||||
unique_together = [('TenantUUID', 'TableCode')]
|
unique_together = [('TenantUUID', 'TableCode')]
|
||||||
managed = False
|
managed = False
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['TenantUUID'], name='idx_cmrate_tenant'),
|
models.Index(fields=['TenantUUID'], name='idx_cmrate_tenant'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.TableCode}:{self.TableName}'
|
return f'{self.TableCode}:{self.TableName}'
|
||||||
|
|
||||||
def GetRate(self, key):
|
def GetRate(self, key):
|
||||||
try:
|
try:
|
||||||
entry = self.Entries.get(RateKey=key, EntryStatus=1)
|
entry = self.Entries.get(RateKey=key, EntryStatus=1)
|
||||||
return entry.RateValue
|
return entry.RateValue
|
||||||
except RateEntry.DoesNotExist:
|
except RateEntry.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def GetRates(self):
|
def GetRates(self):
|
||||||
return {
|
return {
|
||||||
e.RateKey: e.RateValue
|
e.RateKey: e.RateValue
|
||||||
for e in self.Entries.filter(EntryStatus=1)
|
for e in self.Entries.filter(EntryStatus=1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class RateEntry(QModel):
|
class RateEntry(QModel):
|
||||||
"""费率条目——费率表中的一行"""
|
"""费率条目——费率表中的一行"""
|
||||||
|
|
||||||
EntryUUID = UUIDField(primary_key=True)
|
EntryUUID = UUIDField(primary_key=True)
|
||||||
RateTable = models.ForeignKey(
|
RateTable = models.ForeignKey(
|
||||||
RateTable, on_delete=models.CASCADE,
|
RateTable, on_delete=models.CASCADE,
|
||||||
related_name='Entries',
|
related_name='Entries',
|
||||||
)
|
)
|
||||||
RateKey = models.CharField(max_length=64)
|
RateKey = models.CharField(max_length=64)
|
||||||
RateLabel = models.CharField(max_length=128, default='')
|
RateLabel = models.CharField(max_length=128, default='')
|
||||||
RateValue = models.DecimalField(max_digits=10, decimal_places=6)
|
RateValue = models.DecimalField(max_digits=10, decimal_places=6)
|
||||||
EntryStatus = models.SmallIntegerField(default=1)
|
EntryStatus = models.SmallIntegerField(default=1)
|
||||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'CommissionRateEntry'
|
db_table = 'CommissionRateEntry'
|
||||||
app_label = 'gvsdsdk'
|
app_label = 'gvsdsdk'
|
||||||
verbose_name = '费率条目'
|
verbose_name = '费率条目'
|
||||||
verbose_name_plural = '费率条目'
|
verbose_name_plural = '费率条目'
|
||||||
unique_together = [('RateTable', 'RateKey')]
|
unique_together = [('RateTable', 'RateKey')]
|
||||||
managed = False
|
managed = False
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['RateTable', 'RateKey'], name='idx_cmrentry_table_key'),
|
models.Index(fields=['RateTable', 'RateKey'], name='idx_cmrentry_table_key'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.RateKey}={self.RateValue}'
|
return f'{self.RateKey}={self.RateValue}'
|
||||||
|
|
||||||
|
|
||||||
class CommissionRule(QModel):
|
class CommissionRule(QModel):
|
||||||
"""分佣规则表——定义"怎么分"
|
"""分佣规则表——定义"怎么分"
|
||||||
|
|
||||||
每条规则绑定一种事件类型,定义参与者如何分账。
|
每条规则绑定一种事件类型,定义参与者如何分账。
|
||||||
支持比例分账、固定金额、阶梯分账、剩余归平台四种 SplitType。
|
支持比例分账、固定金额、阶梯分账、剩余归平台四种 SplitType。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SPLT_PERCENTAGE = 'percentage'
|
SPLT_PERCENTAGE = 'percentage'
|
||||||
SPLT_FIXED = 'fixed'
|
SPLT_FIXED = 'fixed'
|
||||||
SPLT_TIERED = 'tiered'
|
SPLT_TIERED = 'tiered'
|
||||||
SPLT_REMAINDER = 'remainder'
|
SPLT_REMAINDER = 'remainder'
|
||||||
|
|
||||||
SPLT_TYPE_CHOICES = [
|
SPLT_TYPE_CHOICES = [
|
||||||
(SPLT_PERCENTAGE, '比例分账'),
|
(SPLT_PERCENTAGE, '比例分账'),
|
||||||
(SPLT_FIXED, '固定金额'),
|
(SPLT_FIXED, '固定金额'),
|
||||||
(SPLT_TIERED, '阶梯分账'),
|
(SPLT_TIERED, '阶梯分账'),
|
||||||
(SPLT_REMAINDER, '剩余归平台'),
|
(SPLT_REMAINDER, '剩余归平台'),
|
||||||
]
|
]
|
||||||
|
|
||||||
RuleUUID = UUIDField(primary_key=True)
|
RuleUUID = UUIDField(primary_key=True)
|
||||||
TenantUUID = UUIDField()
|
TenantUUID = UUIDField()
|
||||||
EventType = models.CharField(max_length=32, choices=CommissionEvent.EVT_TYPE_CHOICES)
|
EventType = models.CharField(max_length=32, choices=CommissionEvent.EVT_TYPE_CHOICES)
|
||||||
RuleName = models.CharField(max_length=128)
|
RuleName = models.CharField(max_length=128)
|
||||||
RuleDescription = models.CharField(max_length=512, default='')
|
RuleDescription = models.CharField(max_length=512, default='')
|
||||||
ParticipantRules = models.JSONField(default=list)
|
ParticipantRules = models.JSONField(default=list)
|
||||||
RateTable = models.ForeignKey(
|
RateTable = models.ForeignKey(
|
||||||
RateTable, on_delete=models.SET_NULL,
|
RateTable, on_delete=models.SET_NULL,
|
||||||
null=True, blank=True, related_name='rules',
|
null=True, blank=True, related_name='rules',
|
||||||
)
|
)
|
||||||
MaxInviteLevel = models.SmallIntegerField(default=2)
|
MaxInviteLevel = models.SmallIntegerField(default=2)
|
||||||
RulePriority = models.IntegerField(default=0)
|
RulePriority = models.IntegerField(default=0)
|
||||||
RuleStatus = models.SmallIntegerField(default=1)
|
RuleStatus = models.SmallIntegerField(default=1)
|
||||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
||||||
UpdateTime = models.DateTimeField(null=True, blank=True)
|
UpdateTime = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'CommissionRule'
|
db_table = 'CommissionRule'
|
||||||
app_label = 'gvsdsdk'
|
app_label = 'gvsdsdk'
|
||||||
verbose_name = '分佣规则'
|
verbose_name = '分佣规则'
|
||||||
verbose_name_plural = '分佣规则'
|
verbose_name_plural = '分佣规则'
|
||||||
managed = False
|
managed = False
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['TenantUUID', 'EventType', 'RuleStatus'], name='idx_cmrule_tenant_type_status'),
|
models.Index(fields=['TenantUUID', 'EventType', 'RuleStatus'], name='idx_cmrule_tenant_type_status'),
|
||||||
models.Index(fields=['RulePriority'], name='idx_cmrule_priority'),
|
models.Index(fields=['RulePriority'], name='idx_cmrule_priority'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.RuleName}({self.EventType})'
|
return f'{self.RuleName}({self.EventType})'
|
||||||
|
|
||||||
def FindParticipantRule(self, role):
|
def FindParticipantRule(self, role):
|
||||||
for pr in self.ParticipantRules:
|
for pr in self.ParticipantRules:
|
||||||
if pr.get('role') == role:
|
if pr.get('role') == role:
|
||||||
return pr
|
return pr
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class CommissionRecord(QModel):
|
class CommissionRecord(QModel):
|
||||||
"""分佣记录表——不可变分账流水
|
"""分佣记录表——不可变分账流水
|
||||||
|
|
||||||
每条记录代表一次分账结果:谁、从哪个事件、按什么规则、分了多少。
|
每条记录代表一次分账结果:谁、从哪个事件、按什么规则、分了多少。
|
||||||
生成后即冻结,规则变更不影响历史记录。
|
生成后即冻结,规则变更不影响历史记录。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
REC_PENDING = 0
|
REC_PENDING = 0
|
||||||
REC_SETTLED = 1
|
REC_SETTLED = 1
|
||||||
REC_WITHDRAWN = 2
|
REC_WITHDRAWN = 2
|
||||||
REC_CANCELLED = 3
|
REC_CANCELLED = 3
|
||||||
|
|
||||||
REC_STATUS_CHOICES = [
|
REC_STATUS_CHOICES = [
|
||||||
(REC_PENDING, '待结算'),
|
(REC_PENDING, '待结算'),
|
||||||
(REC_SETTLED, '已结算'),
|
(REC_SETTLED, '已结算'),
|
||||||
(REC_WITHDRAWN, '已提现'),
|
(REC_WITHDRAWN, '已提现'),
|
||||||
(REC_CANCELLED, '已取消'),
|
(REC_CANCELLED, '已取消'),
|
||||||
]
|
]
|
||||||
|
|
||||||
RecordUUID = UUIDField(primary_key=True)
|
RecordUUID = UUIDField(primary_key=True)
|
||||||
TenantUUID = UUIDField()
|
TenantUUID = UUIDField()
|
||||||
Event = models.ForeignKey(
|
Event = models.ForeignKey(
|
||||||
CommissionEvent, on_delete=models.CASCADE,
|
CommissionEvent, on_delete=models.CASCADE,
|
||||||
related_name='records',
|
related_name='records',
|
||||||
)
|
)
|
||||||
Rule = models.ForeignKey(
|
Rule = models.ForeignKey(
|
||||||
CommissionRule, on_delete=models.SET_NULL,
|
CommissionRule, on_delete=models.SET_NULL,
|
||||||
null=True, related_name='records',
|
null=True, related_name='records',
|
||||||
)
|
)
|
||||||
ParticipantID = models.CharField(max_length=128)
|
ParticipantID = models.CharField(max_length=128)
|
||||||
ParticipantRole = models.CharField(max_length=64)
|
ParticipantRole = models.CharField(max_length=64)
|
||||||
SplitType = models.CharField(max_length=16)
|
SplitType = models.CharField(max_length=16)
|
||||||
Amount = models.DecimalField(max_digits=18, decimal_places=4)
|
Amount = models.DecimalField(max_digits=18, decimal_places=4)
|
||||||
RuleSnapshot = models.JSONField(default=dict)
|
RuleSnapshot = models.JSONField(default=dict)
|
||||||
Status = models.SmallIntegerField(default=REC_PENDING, choices=REC_STATUS_CHOICES)
|
Status = models.SmallIntegerField(default=REC_PENDING, choices=REC_STATUS_CHOICES)
|
||||||
TierIndex = models.IntegerField(default=0)
|
TierIndex = models.IntegerField(default=0)
|
||||||
SettleTime = models.DateTimeField(null=True, blank=True)
|
SettleTime = models.DateTimeField(null=True, blank=True)
|
||||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'CommissionRecord'
|
db_table = 'CommissionRecord'
|
||||||
app_label = 'gvsdsdk'
|
app_label = 'gvsdsdk'
|
||||||
verbose_name = '分佣记录'
|
verbose_name = '分佣记录'
|
||||||
verbose_name_plural = '分佣记录'
|
verbose_name_plural = '分佣记录'
|
||||||
ordering = ['-CreateTime']
|
ordering = ['-CreateTime']
|
||||||
managed = False
|
managed = False
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['TenantUUID', 'ParticipantID', 'Status'], name='idx_cmrec_tenant_part_status'),
|
models.Index(fields=['TenantUUID', 'ParticipantID', 'Status'], name='idx_cmrec_tenant_part_status'),
|
||||||
models.Index(fields=['Event'], name='idx_cmrec_event'),
|
models.Index(fields=['Event'], name='idx_cmrec_event'),
|
||||||
models.Index(fields=['CreateTime'], name='idx_cmrec_create_time'),
|
models.Index(fields=['CreateTime'], name='idx_cmrec_CreateTime'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.ParticipantRole}:{self.ParticipantID}={self.Amount}'
|
return f'{self.ParticipantRole}:{self.ParticipantID}={self.Amount}'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def IsSettled(self):
|
def IsSettled(self):
|
||||||
return self.Status >= self.REC_SETTLED
|
return self.Status >= self.REC_SETTLED
|
||||||
|
|
||||||
def Settle(self):
|
def Settle(self):
|
||||||
self.Status = self.REC_SETTLED
|
self.Status = self.REC_SETTLED
|
||||||
self.SettleTime = datetime.datetime.utcnow()
|
self.SettleTime = datetime.datetime.utcnow()
|
||||||
self.save(update_fields=['Status', 'SettleTime'])
|
self.save(update_fields=['Status', 'SettleTime'])
|
||||||
|
|
||||||
def Cancel(self):
|
def Cancel(self):
|
||||||
self.Status = self.REC_CANCELLED
|
self.Status = self.REC_CANCELLED
|
||||||
self.save(update_fields=['Status'])
|
self.save(update_fields=['Status'])
|
||||||
|
|
||||||
|
|
||||||
class PreSettlement(QModel):
|
class PreSettlement(QModel):
|
||||||
"""预结算表——跨平台订单待确认款项
|
"""预结算表——跨平台订单待确认款项
|
||||||
|
|
||||||
当订单涉及外部平台时,打手分成先挂预结算,
|
当订单涉及外部平台时,打手分成先挂预结算,
|
||||||
对方确认后转为正式 CommissionRecord。
|
对方确认后转为正式 CommissionRecord。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PST_PENDING = 0
|
PST_PENDING = 0
|
||||||
PST_CONFIRMED = 1
|
PST_CONFIRMED = 1
|
||||||
PST_REJECTED = 2
|
PST_REJECTED = 2
|
||||||
|
|
||||||
PST_STATUS_CHOICES = [
|
PST_STATUS_CHOICES = [
|
||||||
(PST_PENDING, '待确认'),
|
(PST_PENDING, '待确认'),
|
||||||
(PST_CONFIRMED, '已确认'),
|
(PST_CONFIRMED, '已确认'),
|
||||||
(PST_REJECTED, '已拒绝'),
|
(PST_REJECTED, '已拒绝'),
|
||||||
]
|
]
|
||||||
|
|
||||||
SettlementUUID = UUIDField(primary_key=True)
|
SettlementUUID = UUIDField(primary_key=True)
|
||||||
TenantUUID = UUIDField()
|
TenantUUID = UUIDField()
|
||||||
EventKey = models.CharField(max_length=128)
|
EventKey = models.CharField(max_length=128)
|
||||||
ExternalPlatform = models.CharField(max_length=64, default='')
|
ExternalPlatform = models.CharField(max_length=64, default='')
|
||||||
ExternalOrderID = models.CharField(max_length=128, default='')
|
ExternalOrderID = models.CharField(max_length=128, default='')
|
||||||
ParticipantID = models.CharField(max_length=128)
|
ParticipantID = models.CharField(max_length=128)
|
||||||
ParticipantRole = models.CharField(max_length=64, default='worker')
|
ParticipantRole = models.CharField(max_length=64, default='worker')
|
||||||
Amount = models.DecimalField(max_digits=18, decimal_places=4)
|
Amount = models.DecimalField(max_digits=18, decimal_places=4)
|
||||||
OrderAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
|
OrderAmount = models.DecimalField(max_digits=18, decimal_places=4, default=0)
|
||||||
Status = models.SmallIntegerField(default=PST_PENDING, choices=PST_STATUS_CHOICES)
|
Status = models.SmallIntegerField(default=PST_PENDING, choices=PST_STATUS_CHOICES)
|
||||||
ConfirmedBy = models.CharField(max_length=128, default='')
|
ConfirmedBy = models.CharField(max_length=128, default='')
|
||||||
ConfirmDeadline = models.DateTimeField(null=True, blank=True)
|
ConfirmDeadline = models.DateTimeField(null=True, blank=True)
|
||||||
ConfirmedAt = models.DateTimeField(null=True, blank=True)
|
ConfirmedAt = models.DateTimeField(null=True, blank=True)
|
||||||
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow)
|
||||||
UpdateTime = models.DateTimeField(null=True, blank=True)
|
UpdateTime = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'CommissionPreSettlement'
|
db_table = 'CommissionPreSettlement'
|
||||||
app_label = 'gvsdsdk'
|
app_label = 'gvsdsdk'
|
||||||
verbose_name = '预结算'
|
verbose_name = '预结算'
|
||||||
verbose_name_plural = '预结算'
|
verbose_name_plural = '预结算'
|
||||||
unique_together = [('EventKey', 'ParticipantID')]
|
unique_together = [('EventKey', 'ParticipantID')]
|
||||||
managed = False
|
managed = False
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=['TenantUUID', 'Status'], name='idx_cmpst_tenant_status'),
|
models.Index(fields=['TenantUUID', 'Status'], name='idx_cmpst_tenant_status'),
|
||||||
models.Index(fields=['EventKey'], name='idx_cmpst_event_key'),
|
models.Index(fields=['EventKey'], name='idx_cmpst_event_key'),
|
||||||
models.Index(fields=['ExternalOrderID'], name='idx_cmpst_ext_order'),
|
models.Index(fields=['ExternalOrderID'], name='idx_cmpst_ext_order'),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'预结算:{self.EventKey}->{self.ParticipantID}={self.Amount}'
|
return f'预结算:{self.EventKey}->{self.ParticipantID}={self.Amount}'
|
||||||
|
|
||||||
def Confirm(self, confirmed_by=''):
|
def Confirm(self, confirmed_by=''):
|
||||||
self.Status = self.PST_CONFIRMED
|
self.Status = self.PST_CONFIRMED
|
||||||
self.ConfirmedBy = confirmed_by
|
self.ConfirmedBy = confirmed_by
|
||||||
self.ConfirmedAt = datetime.datetime.utcnow()
|
self.ConfirmedAt = datetime.datetime.utcnow()
|
||||||
self.save(update_fields=['Status', 'ConfirmedBy', 'ConfirmedAt'])
|
self.save(update_fields=['Status', 'ConfirmedBy', 'ConfirmedAt'])
|
||||||
|
|
||||||
def Reject(self, confirmed_by=''):
|
def Reject(self, confirmed_by=''):
|
||||||
self.Status = self.PST_REJECTED
|
self.Status = self.PST_REJECTED
|
||||||
self.ConfirmedBy = confirmed_by
|
self.ConfirmedBy = confirmed_by
|
||||||
self.ConfirmedAt = datetime.datetime.utcnow()
|
self.ConfirmedAt = datetime.datetime.utcnow()
|
||||||
self.save(update_fields=['Status', 'ConfirmedBy', 'ConfirmedAt'])
|
self.save(update_fields=['Status', 'ConfirmedBy', 'ConfirmedAt'])
|
||||||
|
|||||||
1662
gvsdsdk/fluent.py
1662
gvsdsdk/fluent.py
File diff suppressed because it is too large
Load Diff
30
gvsdsdk/management/commands/hash_passwords.py
Normal file
30
gvsdsdk/management/commands/hash_passwords.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
"""将 User 表中明文密码迁移为 bcrypt 哈希"""
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = '将 User 表中所有明文密码迁移为 bcrypt 哈希'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# 筛选非空且非 bcrypt 哈希的密码
|
||||||
|
users = User.objects.exclude(UserPassword='')
|
||||||
|
updated = 0
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
for user in users.iterator():
|
||||||
|
stored = user.UserPassword or ''
|
||||||
|
# 已是 bcrypt 哈希则跳过
|
||||||
|
if stored.startswith(('$2b$', '$2a$')) and len(stored) == 60:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
# 明文密码 → bcrypt 哈希
|
||||||
|
user.SetPassword(stored)
|
||||||
|
user.save(update_fields=['UserPassword'])
|
||||||
|
updated += 1
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f'完成:{updated} 条明文密码已转为 bcrypt,{skipped} 条已是哈希跳过'
|
||||||
|
))
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Generated by Django 6.0.5 on 2026-06-13 11:43
|
# Generated by Django 4.2.27 on 2026-06-19 18:42
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import gvsdsdk.models
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import gvsdsdk.models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -16,9 +16,9 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='AccountActivation',
|
name='AccountActivation',
|
||||||
fields=[
|
fields=[
|
||||||
('AccountActivationCode', gvsdsdk.models.FixedBinaryField(binary_length=6, db_column='AccountActivationCode', max_length=6, primary_key=True, serialize=False)),
|
('AccountActivationCode', gvsdsdk.models.FixedBinaryField(binary_length=6, db_column='AccountActivationCode', max_length=6, primary_key=True, serialize=False, verbose_name='激活码')),
|
||||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')),
|
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
|
||||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '账户激活',
|
'verbose_name': '账户激活',
|
||||||
@@ -30,13 +30,13 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='EphemeralToken',
|
name='EphemeralToken',
|
||||||
fields=[
|
fields=[
|
||||||
('ELTUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUUID', primary_key=True, serialize=False)),
|
('ELTUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUUID', primary_key=True, serialize=False, verbose_name='令牌UUID')),
|
||||||
('ELTUserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUserUUID', db_index=True)),
|
('ELTUserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ELTUserUUID', db_index=True, verbose_name='用户UUID')),
|
||||||
('ELTCreateTime', models.DateTimeField(db_column='ELTCreateTime', default=datetime.datetime.utcnow)),
|
('ELTCreateTime', models.DateTimeField(db_column='ELTCreateTime', default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
('ELTEndTime', models.DateTimeField(db_column='ELTEndTime', default=gvsdsdk.models._elt_end_time_default)),
|
('ELTEndTime', models.DateTimeField(db_column='ELTEndTime', default=gvsdsdk.models._EltEndTimeDefault, verbose_name='过期时间')),
|
||||||
('ELTType', models.SmallIntegerField(db_column='ELTType', default=0)),
|
('ELTType', models.SmallIntegerField(db_column='ELTType', default=0, verbose_name='令牌类型')),
|
||||||
('ELTAllowIP', models.JSONField(db_column='ELTAllowIP', default=list)),
|
('ELTAllowIP', models.JSONField(db_column='ELTAllowIP', default=list, verbose_name='允许IP')),
|
||||||
('ELTStatus', models.SmallIntegerField(db_column='ELTStatus', db_index=True, default=1)),
|
('ELTStatus', models.SmallIntegerField(db_column='ELTStatus', db_index=True, default=1, verbose_name='状态')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '临时令牌',
|
'verbose_name': '临时令牌',
|
||||||
@@ -48,8 +48,8 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Password',
|
name='Password',
|
||||||
fields=[
|
fields=[
|
||||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)),
|
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False, verbose_name='用户UUID')),
|
||||||
('Password', models.CharField(db_column='Password', max_length=1024)),
|
('Password', models.CharField(db_column='Password', max_length=1024, verbose_name='密码哈希')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '密码',
|
'verbose_name': '密码',
|
||||||
@@ -61,12 +61,14 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Permission',
|
name='Permission',
|
||||||
fields=[
|
fields=[
|
||||||
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', primary_key=True, serialize=False)),
|
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', primary_key=True, serialize=False, verbose_name='权限UUID')),
|
||||||
('PermName', models.CharField(db_column='PermName', max_length=128)),
|
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
|
||||||
('PermCode', models.CharField(db_column='PermCode', max_length=128)),
|
('PermName', models.CharField(db_column='PermName', max_length=128, verbose_name='权限名称')),
|
||||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
|
('PermCode', models.CharField(db_column='PermCode', max_length=128, verbose_name='权限键')),
|
||||||
('PermStatus', models.SmallIntegerField(db_column='PermStatus', default=1)),
|
('PermCategory', models.CharField(db_column='PermCategory', default='', max_length=64, verbose_name='权限分类')),
|
||||||
('PermDesc', models.CharField(db_column='PermDesc', default='', max_length=512)),
|
('PermStatus', models.SmallIntegerField(db_column='PermStatus', default=1, verbose_name='是否启用')),
|
||||||
|
('PermDesc', models.CharField(db_column='PermDesc', default='', max_length=512, verbose_name='描述')),
|
||||||
|
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '权限',
|
'verbose_name': '权限',
|
||||||
@@ -75,16 +77,72 @@ class Migration(migrations.Migration):
|
|||||||
'managed': True,
|
'managed': True,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Position',
|
||||||
|
fields=[
|
||||||
|
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionUUID', primary_key=True, serialize=False, verbose_name='职务UUID')),
|
||||||
|
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
|
||||||
|
('PositionName', models.CharField(db_column='PositionName', max_length=128, verbose_name='职务名称')),
|
||||||
|
('PositionCode', models.CharField(db_column='PositionCode', default='', max_length=64, verbose_name='职务编码')),
|
||||||
|
('PositionLevel', models.SmallIntegerField(db_column='PositionLevel', default=0, verbose_name='职务级别')),
|
||||||
|
('PositionStatus', models.SmallIntegerField(db_column='PositionStatus', default=1, verbose_name='状态')),
|
||||||
|
('PositionDesc', models.CharField(db_column='PositionDesc', default='', max_length=512, verbose_name='描述')),
|
||||||
|
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='更新时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '人事职务标签',
|
||||||
|
'verbose_name_plural': '人事职务标签',
|
||||||
|
'db_table': 'Position',
|
||||||
|
'managed': True,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PositionTemplate',
|
||||||
|
fields=[
|
||||||
|
('PositionTemplateUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionTemplateUUID', primary_key=True, serialize=False, verbose_name='岗位模板UUID')),
|
||||||
|
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
|
||||||
|
('TemplateName', models.CharField(db_column='TemplateName', max_length=128, verbose_name='模板名称')),
|
||||||
|
('TemplateCode', models.CharField(db_column='TemplateCode', default='', max_length=64, verbose_name='模板编码')),
|
||||||
|
('TemplateStatus', models.SmallIntegerField(db_column='TemplateStatus', default=1, verbose_name='模板状态')),
|
||||||
|
('TemplateDesc', models.CharField(db_column='TemplateDesc', default='', max_length=512, verbose_name='模板描述')),
|
||||||
|
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='更新时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '岗位模板',
|
||||||
|
'verbose_name_plural': '岗位模板',
|
||||||
|
'db_table': 'PositionTemplate',
|
||||||
|
'managed': True,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PositionTemplatePermission',
|
||||||
|
fields=[
|
||||||
|
('PTPermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PTPermUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||||
|
('PositionTemplateUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionTemplateUUID', verbose_name='岗位模板UUID')),
|
||||||
|
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', verbose_name='权限UUID')),
|
||||||
|
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '岗位模板权限关联',
|
||||||
|
'verbose_name_plural': '岗位模板权限关联',
|
||||||
|
'db_table': 'PositionTemplatePermission',
|
||||||
|
'managed': True,
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Role',
|
name='Role',
|
||||||
fields=[
|
fields=[
|
||||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', primary_key=True, serialize=False)),
|
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', primary_key=True, serialize=False, verbose_name='角色UUID')),
|
||||||
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')),
|
('TenantUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='TenantUUID', verbose_name='租户UUID')),
|
||||||
('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='DeptUUID', null=True)),
|
('RoleName', models.CharField(db_column='RoleName', max_length=128, verbose_name='角色名称')),
|
||||||
('RoleName', models.CharField(db_column='RoleName', max_length=128)),
|
('RoleType', models.SmallIntegerField(db_column='RoleType', default=2, verbose_name='角色类型')),
|
||||||
('RoleType', models.SmallIntegerField(db_column='RoleType', default=2)),
|
('RoleStatus', models.SmallIntegerField(db_column='RoleStatus', default=1, verbose_name='角色状态')),
|
||||||
('RoleStatus', models.SmallIntegerField(db_column='RoleStatus', default=1)),
|
('AssignScope', models.SmallIntegerField(db_column='AssignScope', default=0, verbose_name='分配可见范围')),
|
||||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
('AssignScopeUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='AssignScopeUUID', null=True, verbose_name='分配范围UUID')),
|
||||||
|
('RoleDesc', models.CharField(db_column='RoleDesc', default='', max_length=512, verbose_name='角色描述')),
|
||||||
|
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '角色',
|
'verbose_name': '角色',
|
||||||
@@ -94,55 +152,69 @@ class Migration(migrations.Migration):
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='RoleDept',
|
name='RolePermission',
|
||||||
fields=[
|
fields=[
|
||||||
('RoleDeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleDeptUUID', primary_key=True, serialize=False)),
|
('RolePermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RolePermUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
|
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')),
|
||||||
('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='DeptUUID')),
|
('PermUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PermUUID', verbose_name='权限UUID')),
|
||||||
('IsMainDept', models.SmallIntegerField(db_column='IsMainDept', default=1)),
|
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '角色部门关联',
|
'verbose_name': '角色权限关联',
|
||||||
'verbose_name_plural': '角色部门关联',
|
'verbose_name_plural': '角色权限关联',
|
||||||
'db_table': 'RoleDept',
|
'db_table': 'RolePermission',
|
||||||
'managed': True,
|
'managed': True,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='User',
|
name='RoleShop',
|
||||||
fields=[
|
fields=[
|
||||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)),
|
('RoleShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleShopUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||||
('UserName', models.CharField(db_column='UserName', max_length=256, unique=True)),
|
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')),
|
||||||
('UserPassword', models.CharField(db_column='UserPassword', default='', max_length=60)),
|
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', verbose_name='商铺UUID')),
|
||||||
('UserPositionStatus', models.SmallIntegerField(db_column='UserPositionStatus', default=0)),
|
('IsMainShop', models.SmallIntegerField(db_column='IsMainShop', default=0, verbose_name='主商铺')),
|
||||||
('UserPersonalSignature', models.CharField(db_column='UserPersonalSignature', default='', max_length=1024)),
|
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
('UserEmail', models.CharField(db_column='UserEmail', default='', max_length=256)),
|
|
||||||
('UserEmailVerify', models.SmallIntegerField(db_column='UserEmailVerify', default=0)),
|
|
||||||
('UserAccountLicense', models.SmallIntegerField(db_column='UserAccountLicense', default=1)),
|
|
||||||
('UserGender', models.SmallIntegerField(db_column='UserGender', default=-1)),
|
|
||||||
('UserTele', models.CharField(blank=True, db_column='UserTele', max_length=16, null=True)),
|
|
||||||
('UserCreateTime', models.DateTimeField(db_column='UserCreateTime', default=datetime.datetime.utcnow)),
|
|
||||||
('UserLastLoginDate', models.DateTimeField(db_column='UserLastLoginDate', default=datetime.datetime.utcnow)),
|
|
||||||
('IsStaff', models.BooleanField(db_column='IsStaff', default=False)),
|
|
||||||
('IsSuperuser', models.BooleanField(db_column='IsSuperuser', default=False)),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '用户',
|
'verbose_name': '角色商铺关联',
|
||||||
'verbose_name_plural': '用户',
|
'verbose_name_plural': '角色商铺关联',
|
||||||
'db_table': 'User',
|
'db_table': 'RoleShop',
|
||||||
|
'managed': True,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Shop',
|
||||||
|
fields=[
|
||||||
|
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', primary_key=True, serialize=False, verbose_name='商铺UUID')),
|
||||||
|
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID', verbose_name='公司UUID')),
|
||||||
|
('ShopName', models.CharField(db_column='ShopName', max_length=128, verbose_name='商铺名称')),
|
||||||
|
('ShopCode', models.CharField(db_column='ShopCode', default='', max_length=64, verbose_name='商铺编码')),
|
||||||
|
('ShopType', models.SmallIntegerField(db_column='ShopType', default=0, verbose_name='商铺类型')),
|
||||||
|
('ShopStatus', models.SmallIntegerField(db_column='ShopStatus', default=1, verbose_name='商铺状态')),
|
||||||
|
('ShopAddress', models.CharField(db_column='ShopAddress', default='', max_length=512, verbose_name='商铺地址')),
|
||||||
|
('ShopContact', models.CharField(db_column='ShopContact', default='', max_length=128, verbose_name='联系人')),
|
||||||
|
('ShopPhone', models.CharField(db_column='ShopPhone', default='', max_length=32, verbose_name='联系电话')),
|
||||||
|
('ShopDesc', models.CharField(db_column='ShopDesc', default='', max_length=512, verbose_name='描述')),
|
||||||
|
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
|
('UpdateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='更新时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '商铺',
|
||||||
|
'verbose_name_plural': '商铺',
|
||||||
|
'db_table': 'Shop',
|
||||||
'managed': True,
|
'managed': True,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='User2FA',
|
name='User2FA',
|
||||||
fields=[
|
fields=[
|
||||||
('User2FAUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='User2FAUUID', primary_key=True, serialize=False)),
|
('User2FAUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='User2FAUUID', primary_key=True, serialize=False, verbose_name='2FA UUID')),
|
||||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True)),
|
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True, verbose_name='用户UUID')),
|
||||||
('SecretKey', models.CharField(db_column='SecretKey', max_length=64)),
|
('SecretKey', models.CharField(db_column='SecretKey', max_length=64, verbose_name='密钥')),
|
||||||
('IsEnabled', models.SmallIntegerField(db_column='IsEnabled', default=0)),
|
('IsEnabled', models.SmallIntegerField(db_column='IsEnabled', default=0, verbose_name='是否启用')),
|
||||||
('Verified', models.SmallIntegerField(db_column='Verified', default=0)),
|
('Verified', models.SmallIntegerField(db_column='Verified', default=0, verbose_name='已验证')),
|
||||||
('LastUsedAt', models.DateTimeField(blank=True, db_column='LastUsedAt', null=True)),
|
('LastUsedAt', models.DateTimeField(blank=True, db_column='LastUsedAt', null=True, verbose_name='最后使用')),
|
||||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '双因素认证',
|
'verbose_name': '双因素认证',
|
||||||
@@ -154,9 +226,9 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='UserABAC',
|
name='UserABAC',
|
||||||
fields=[
|
fields=[
|
||||||
('UserABACUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserABACUUID', primary_key=True, serialize=False)),
|
('UserABACUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserABACUUID', primary_key=True, serialize=False, verbose_name='ABAC UUID')),
|
||||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')),
|
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
|
||||||
('HierarchicalPermissionIdentifiers', models.CharField(db_column='HierarchicalPermissionIdentifiers', max_length=256)),
|
('HierarchicalPermissionIdentifiers', models.CharField(db_column='HierarchicalPermissionIdentifiers', max_length=256, verbose_name='层级权限标识')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '用户ABAC',
|
'verbose_name': '用户ABAC',
|
||||||
@@ -168,10 +240,10 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='UserAvatar',
|
name='UserAvatar',
|
||||||
fields=[
|
fields=[
|
||||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False)),
|
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', primary_key=True, serialize=False, verbose_name='用户UUID')),
|
||||||
('UserAvatar', models.BinaryField(db_column='UserAvatar', editable=True)),
|
('UserAvatar', models.BinaryField(db_column='UserAvatar', editable=True, verbose_name='头像数据')),
|
||||||
('AvatarUploadTime', models.DateTimeField(db_column='AvatarUploadTime', default=datetime.datetime.utcnow)),
|
('AvatarUploadTime', models.DateTimeField(db_column='AvatarUploadTime', default=datetime.datetime.utcnow, verbose_name='上传时间')),
|
||||||
('AvatarFileType', models.CharField(db_column='AvatarFileType', default='png', max_length=16)),
|
('AvatarFileType', models.CharField(db_column='AvatarFileType', default='png', max_length=16, verbose_name='文件类型')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '用户头像',
|
'verbose_name': '用户头像',
|
||||||
@@ -180,16 +252,33 @@ class Migration(migrations.Migration):
|
|||||||
'managed': True,
|
'managed': True,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserDept',
|
||||||
|
fields=[
|
||||||
|
('UserDeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserDeptUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||||
|
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
|
||||||
|
('DeptUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='DeptUUID', verbose_name='部门UUID')),
|
||||||
|
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, blank=True, db_column='PositionUUID', null=True, verbose_name='职务标签UUID')),
|
||||||
|
('IsMainDept', models.SmallIntegerField(db_column='IsMainDept', default=1, verbose_name='是否主部门')),
|
||||||
|
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '员工部门关联',
|
||||||
|
'verbose_name_plural': '员工部门关联',
|
||||||
|
'db_table': 'UserDept',
|
||||||
|
'managed': True,
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='UserLoginLog',
|
name='UserLoginLog',
|
||||||
fields=[
|
fields=[
|
||||||
('LogUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='LogUUID', primary_key=True, serialize=False)),
|
('LogUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='LogUUID', primary_key=True, serialize=False, verbose_name='日志UUID')),
|
||||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True)),
|
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', db_index=True, verbose_name='用户UUID')),
|
||||||
('LoginIP', models.CharField(db_column='LoginIP', max_length=39)),
|
('LoginIP', models.CharField(db_column='LoginIP', max_length=39, verbose_name='登录IP')),
|
||||||
('LoginPlatform', models.CharField(db_column='LoginPlatform', max_length=32)),
|
('LoginPlatform', models.CharField(db_column='LoginPlatform', max_length=32, verbose_name='登录平台')),
|
||||||
('LoginStatus', models.SmallIntegerField(db_column='LoginStatus')),
|
('LoginStatus', models.SmallIntegerField(db_column='LoginStatus', verbose_name='登录状态')),
|
||||||
('LoginTime', models.DateTimeField(db_column='LoginTime', default=datetime.datetime.utcnow)),
|
('LoginTime', models.DateTimeField(db_column='LoginTime', default=datetime.datetime.utcnow, verbose_name='登录时间')),
|
||||||
('UserAgent', models.CharField(db_column='UserAgent', default='', max_length=512)),
|
('UserAgent', models.CharField(db_column='UserAgent', default='', max_length=512, verbose_name='用户代理')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '登录日志',
|
'verbose_name': '登录日志',
|
||||||
@@ -201,10 +290,10 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='UserPending2FA',
|
name='UserPending2FA',
|
||||||
fields=[
|
fields=[
|
||||||
('PendingUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PendingUUID', primary_key=True, serialize=False)),
|
('PendingUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PendingUUID', primary_key=True, serialize=False, verbose_name='待确认UUID')),
|
||||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', unique=True)),
|
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', unique=True, verbose_name='用户UUID')),
|
||||||
('SecretKey', models.CharField(db_column='SecretKey', max_length=64)),
|
('SecretKey', models.CharField(db_column='SecretKey', max_length=64, verbose_name='密钥')),
|
||||||
('ExpireAt', models.DateTimeField(db_column='ExpireAt')),
|
('ExpireAt', models.DateTimeField(db_column='ExpireAt', verbose_name='过期时间')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '2FA待确认',
|
'verbose_name': '2FA待确认',
|
||||||
@@ -216,10 +305,11 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='UserRole',
|
name='UserRole',
|
||||||
fields=[
|
fields=[
|
||||||
('UserRoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserRoleUUID', primary_key=True, serialize=False)),
|
('UserRoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserRoleUUID', primary_key=True, serialize=False, verbose_name='关联UUID')),
|
||||||
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID')),
|
('UserUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='UserUUID', verbose_name='用户UUID')),
|
||||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
|
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID', verbose_name='角色UUID')),
|
||||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID', verbose_name='公司UUID')),
|
||||||
|
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': '用户角色关联',
|
'verbose_name': '用户角色关联',
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
# Generated by Django 6.0.5 on 2026-06-13 12:29
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import gvsdsdk.models
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('gvsdsdk', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Position',
|
|
||||||
fields=[
|
|
||||||
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionUUID', primary_key=True, serialize=False)),
|
|
||||||
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')),
|
|
||||||
('PositionName', models.CharField(db_column='PositionName', max_length=128)),
|
|
||||||
('PositionCode', models.CharField(db_column='PositionCode', default='', max_length=64)),
|
|
||||||
('PositionLevel', models.SmallIntegerField(db_column='PositionLevel', default=0)),
|
|
||||||
('PositionStatus', models.SmallIntegerField(db_column='PositionStatus', default=1)),
|
|
||||||
('PositionDesc', models.CharField(db_column='PositionDesc', default='', max_length=512)),
|
|
||||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
|
||||||
('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '职务',
|
|
||||||
'verbose_name_plural': '职务',
|
|
||||||
'db_table': 'Position',
|
|
||||||
'managed': True,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='RolePosition',
|
|
||||||
fields=[
|
|
||||||
('RolePositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RolePositionUUID', primary_key=True, serialize=False)),
|
|
||||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
|
|
||||||
('PositionUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='PositionUUID')),
|
|
||||||
('IsMainPosition', models.SmallIntegerField(db_column='IsMainPosition', default=0)),
|
|
||||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '角色职务关联',
|
|
||||||
'verbose_name_plural': '角色职务关联',
|
|
||||||
'db_table': 'RolePosition',
|
|
||||||
'managed': True,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='RoleShop',
|
|
||||||
fields=[
|
|
||||||
('RoleShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleShopUUID', primary_key=True, serialize=False)),
|
|
||||||
('RoleUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='RoleUUID')),
|
|
||||||
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID')),
|
|
||||||
('IsMainShop', models.SmallIntegerField(db_column='IsMainShop', default=0)),
|
|
||||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '角色商铺关联',
|
|
||||||
'verbose_name_plural': '角色商铺关联',
|
|
||||||
'db_table': 'RoleShop',
|
|
||||||
'managed': True,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Shop',
|
|
||||||
fields=[
|
|
||||||
('ShopUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='ShopUUID', primary_key=True, serialize=False)),
|
|
||||||
('CompanyUUID', gvsdsdk.models.UUIDField(binary_length=16, db_column='CompanyUUID')),
|
|
||||||
('ShopName', models.CharField(db_column='ShopName', max_length=128)),
|
|
||||||
('ShopCode', models.CharField(db_column='ShopCode', default='', max_length=64)),
|
|
||||||
('ShopType', models.SmallIntegerField(db_column='ShopType', default=0)),
|
|
||||||
('ShopStatus', models.SmallIntegerField(db_column='ShopStatus', default=1)),
|
|
||||||
('ShopAddress', models.CharField(db_column='ShopAddress', default='', max_length=512)),
|
|
||||||
('ShopContact', models.CharField(db_column='ShopContact', default='', max_length=128)),
|
|
||||||
('ShopPhone', models.CharField(db_column='ShopPhone', default='', max_length=32)),
|
|
||||||
('ShopDesc', models.CharField(db_column='ShopDesc', default='', max_length=512)),
|
|
||||||
('CreateTime', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
|
|
||||||
('UpdateTime', models.DateTimeField(db_column='UpdateTime', default=datetime.datetime.utcnow)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '商铺',
|
|
||||||
'verbose_name_plural': '商铺',
|
|
||||||
'db_table': 'Shop',
|
|
||||||
'managed': True,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
File diff suppressed because it is too large
Load Diff
1643
gvsdsdk/models.py
1643
gvsdsdk/models.py
File diff suppressed because it is too large
Load Diff
532
gvsdsdk/msync/orm_handler.py
Normal file
532
gvsdsdk/msync/orm_handler.py
Normal file
@@ -0,0 +1,532 @@
|
|||||||
|
"""MSYNC ORM 中转处理器
|
||||||
|
|
||||||
|
子服务器端注册 Socket.IO 事件处理器,接收主服务器发来的 ORM 查询请求,
|
||||||
|
在本地数据库执行 SQL 并返回结果。
|
||||||
|
|
||||||
|
用法(子服务器端):
|
||||||
|
from gvsdsdk.msync.orm_handler import ORMRelayHandler
|
||||||
|
|
||||||
|
handler = ORMRelayHandler()
|
||||||
|
handler.Register(sio_client) # 注册到 MSyncSocketClient 实例
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ORMRelayHandler:
|
||||||
|
"""ORM 中转处理器——在子服务器端执行本地 SQL 查询
|
||||||
|
|
||||||
|
通过 MSYNC Socket.IO 事件协议,主服务器将 ORM 请求转发给子服务器,
|
||||||
|
子服务器在本地数据库执行查询后返回结果。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def Register(self, sio_client):
|
||||||
|
"""将 ORM 事件处理器注册到 MSyncSocketClient 实例
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sio_client: MSyncSocketClient 实例,需要有 _sio 属性
|
||||||
|
"""
|
||||||
|
sio = sio_client._sio
|
||||||
|
|
||||||
|
@sio.on('msync:orm:tables', namespace='/msync')
|
||||||
|
def on_orm_tables(data):
|
||||||
|
return self.HandleTables(data)
|
||||||
|
|
||||||
|
@sio.on('msync:orm:columns', namespace='/msync')
|
||||||
|
def on_orm_columns(data):
|
||||||
|
return self.HandleColumns(data)
|
||||||
|
|
||||||
|
@sio.on('msync:orm:list', namespace='/msync')
|
||||||
|
def on_orm_list(data):
|
||||||
|
return self.HandleList(data)
|
||||||
|
|
||||||
|
@sio.on('msync:orm:add', namespace='/msync')
|
||||||
|
def on_orm_add(data):
|
||||||
|
return self.HandleAdd(data)
|
||||||
|
|
||||||
|
@sio.on('msync:orm:modify', namespace='/msync')
|
||||||
|
def on_orm_modify(data):
|
||||||
|
return self.HandleModify(data)
|
||||||
|
|
||||||
|
@sio.on('msync:orm:delete', namespace='/msync')
|
||||||
|
def on_orm_delete(data):
|
||||||
|
return self.HandleDelete(data)
|
||||||
|
|
||||||
|
logger.info('[ORM] ORM 中转处理器已注册')
|
||||||
|
|
||||||
|
def _GetConnection(self):
|
||||||
|
"""获取本地数据库连接(Django default)"""
|
||||||
|
from django.db import connections
|
||||||
|
conn = connections['default']
|
||||||
|
conn.ensure_connection()
|
||||||
|
return conn
|
||||||
|
|
||||||
|
# ─── 事件处理 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def HandleTables(self, data):
|
||||||
|
"""列出本地数据库中所有用户表"""
|
||||||
|
try:
|
||||||
|
conn = self._GetConnection()
|
||||||
|
except Exception as e:
|
||||||
|
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
tables = self._ListTables(cursor)
|
||||||
|
return {'code': 0, 'msg': 'success', 'data': tables}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'[ORM] HandleTables 异常: {e}')
|
||||||
|
return {'code': 1, 'msg': f'查询失败: {e}'}
|
||||||
|
|
||||||
|
def HandleColumns(self, data):
|
||||||
|
"""列出指定表的列信息"""
|
||||||
|
table_name = (data or {}).get('Table', '')
|
||||||
|
if not table_name:
|
||||||
|
return {'code': 1, 'msg': '缺少 Table 参数'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = self._GetConnection()
|
||||||
|
except Exception as e:
|
||||||
|
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
columns = self._ListColumns(cursor, table_name)
|
||||||
|
return {'code': 0, 'msg': 'success', 'data': columns}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'[ORM] HandleColumns 异常: {e}')
|
||||||
|
return {'code': 1, 'msg': f'查询失败: {e}'}
|
||||||
|
|
||||||
|
def HandleList(self, data):
|
||||||
|
"""分页查询表数据"""
|
||||||
|
table_name = (data or {}).get('Table', '')
|
||||||
|
if not table_name:
|
||||||
|
return {'code': 1, 'msg': '缺少 Table 参数'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = self._GetConnection()
|
||||||
|
except Exception as e:
|
||||||
|
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
result = self._QueryTable(cursor, table_name, data or {})
|
||||||
|
if result.get('error'):
|
||||||
|
return {'code': 1, 'msg': result['error'], 'data': None}
|
||||||
|
return {'code': 0, 'msg': 'success', 'data': result}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'[ORM] HandleList 异常: {e}')
|
||||||
|
return {'code': 1, 'msg': f'查询失败: {e}'}
|
||||||
|
|
||||||
|
def HandleAdd(self, data):
|
||||||
|
"""新增记录"""
|
||||||
|
table_name = (data or {}).get('Table', '')
|
||||||
|
record = (data or {}).get('Record', {})
|
||||||
|
if not table_name:
|
||||||
|
return {'code': 1, 'msg': '缺少 Table 参数'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = self._GetConnection()
|
||||||
|
except Exception as e:
|
||||||
|
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
result = self._AddRecord(cursor, table_name, record)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'[ORM] HandleAdd 异常: {e}')
|
||||||
|
return {'code': 1, 'msg': f'新增失败: {e}'}
|
||||||
|
|
||||||
|
def HandleModify(self, data):
|
||||||
|
"""修改记录"""
|
||||||
|
table_name = (data or {}).get('Table', '')
|
||||||
|
record = (data or {}).get('Record', {})
|
||||||
|
if not table_name:
|
||||||
|
return {'code': 1, 'msg': '缺少 Table 参数'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = self._GetConnection()
|
||||||
|
except Exception as e:
|
||||||
|
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
result = self._ModifyRecord(cursor, table_name, record)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'[ORM] HandleModify 异常: {e}')
|
||||||
|
return {'code': 1, 'msg': f'修改失败: {e}'}
|
||||||
|
|
||||||
|
def HandleDelete(self, data):
|
||||||
|
"""删除记录"""
|
||||||
|
table_name = (data or {}).get('Table', '')
|
||||||
|
record = (data or {}).get('Record', {})
|
||||||
|
if not table_name:
|
||||||
|
return {'code': 1, 'msg': '缺少 Table 参数'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = self._GetConnection()
|
||||||
|
except Exception as e:
|
||||||
|
return {'code': 1, 'msg': f'数据库连接失败: {e}'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cursor:
|
||||||
|
result = self._DeleteRecord(cursor, table_name, record)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'[ORM] HandleDelete 异常: {e}')
|
||||||
|
return {'code': 1, 'msg': f'删除失败: {e}'}
|
||||||
|
|
||||||
|
# ─── SQL 执行 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _ListTables(self, cursor):
|
||||||
|
"""列出所有用户表,含 i18n 标题"""
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT TABLE_NAME, TABLE_ROWS, TABLE_COMMENT FROM information_schema.TABLES "
|
||||||
|
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE' "
|
||||||
|
"ORDER BY TABLE_NAME"
|
||||||
|
)
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
# 构建表名→中文标题映射
|
||||||
|
model_verbose = {}
|
||||||
|
for model in apps.get_models():
|
||||||
|
meta = model._meta
|
||||||
|
# 优先使用 table_title(自定义 Meta 属性),其次 verbose_name
|
||||||
|
table_title = getattr(meta, 'table_title', None)
|
||||||
|
title = str(table_title) if table_title else (str(meta.verbose_name) if meta.verbose_name else '')
|
||||||
|
if title:
|
||||||
|
model_verbose[meta.object_name] = title
|
||||||
|
model_verbose[meta.db_table] = title
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
table_name, table_rows, table_comment = row
|
||||||
|
# 优先级:TABLE_COMMENT > table_title/verbose_name > 表名
|
||||||
|
title = table_comment or model_verbose.get(table_name) or table_name
|
||||||
|
result.append({'name': table_name, 'title': title, 'count': table_rows or 0})
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _ListColumns(self, cursor, table_name):
|
||||||
|
"""列出指定表的列信息"""
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_KEY, EXTRA, COLUMN_TYPE, COLUMN_COMMENT "
|
||||||
|
"FROM information_schema.COLUMNS "
|
||||||
|
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s "
|
||||||
|
"ORDER BY ORDINAL_POSITION",
|
||||||
|
[table_name]
|
||||||
|
)
|
||||||
|
|
||||||
|
model_verbose = self._FindModelVerboseNames(table_name)
|
||||||
|
logger.info(f'[ORM] _ListColumns table={table_name} model_verbose={model_verbose}')
|
||||||
|
|
||||||
|
columns = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
col_name, data_type, is_nullable, col_key, extra, col_type, col_comment = row
|
||||||
|
is_pk = col_key == 'PRI'
|
||||||
|
is_auto = 'auto_increment' in (extra or '').lower()
|
||||||
|
|
||||||
|
if data_type in ('int', 'bigint', 'tinyint', 'smallint', 'mediumint'):
|
||||||
|
ftype = 'number'
|
||||||
|
elif data_type in ('float', 'double', 'decimal'):
|
||||||
|
ftype = 'number'
|
||||||
|
elif data_type in ('datetime', 'timestamp'):
|
||||||
|
ftype = 'datetime'
|
||||||
|
elif data_type in ('date',):
|
||||||
|
ftype = 'date'
|
||||||
|
elif data_type in ('time',):
|
||||||
|
ftype = 'time'
|
||||||
|
elif data_type in ('json',):
|
||||||
|
ftype = 'textarea'
|
||||||
|
elif data_type in ('text', 'mediumtext', 'longtext', 'tinytext'):
|
||||||
|
ftype = 'textarea'
|
||||||
|
elif data_type in ('binary', 'varbinary', 'blob'):
|
||||||
|
ftype = 'uuid'
|
||||||
|
else:
|
||||||
|
ftype = 'text'
|
||||||
|
|
||||||
|
title = col_comment or model_verbose.get(col_name) or col_name
|
||||||
|
|
||||||
|
columns.append({
|
||||||
|
'field': col_name,
|
||||||
|
'title': title,
|
||||||
|
'type': ftype,
|
||||||
|
'editable': not is_pk and not is_auto,
|
||||||
|
'sortable': True,
|
||||||
|
'filterable': True,
|
||||||
|
'primaryKey': is_pk,
|
||||||
|
'hideInForm': is_pk,
|
||||||
|
'required': is_nullable == 'NO' and not is_auto,
|
||||||
|
'dbType': col_type,
|
||||||
|
})
|
||||||
|
return columns
|
||||||
|
|
||||||
|
def _QueryTable(self, cursor, table_name, params):
|
||||||
|
"""分页查询表数据"""
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT COUNT(*) FROM information_schema.TABLES "
|
||||||
|
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s",
|
||||||
|
[table_name]
|
||||||
|
)
|
||||||
|
if cursor.fetchone()[0] == 0:
|
||||||
|
return {'records': [], 'total': 0, 'error': f'表不存在: {table_name}'}
|
||||||
|
|
||||||
|
col_info = self._GetTableColumns(cursor, table_name)
|
||||||
|
pk_cols = {k: v for k, v in col_info.items() if v['is_pk']}
|
||||||
|
|
||||||
|
where_parts = []
|
||||||
|
where_values = []
|
||||||
|
for pk_name in pk_cols:
|
||||||
|
pk_val = params.get(pk_name, '')
|
||||||
|
if pk_val:
|
||||||
|
where_parts.append(f'`{pk_name}` = %s')
|
||||||
|
where_values.append(self._ConvertValueForDB(pk_val, pk_cols[pk_name]))
|
||||||
|
|
||||||
|
keyword = params.get('keyword', '')
|
||||||
|
if not where_parts and keyword:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT COLUMN_NAME FROM information_schema.COLUMNS "
|
||||||
|
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s "
|
||||||
|
"AND DATA_TYPE IN ('varchar', 'char', 'text', 'mediumtext', 'longtext')",
|
||||||
|
[table_name]
|
||||||
|
)
|
||||||
|
text_cols = [row[0] for row in cursor.fetchall()]
|
||||||
|
if text_cols:
|
||||||
|
conditions = [f'`{c}` LIKE %s' for c in text_cols]
|
||||||
|
or_clause = ' OR '.join(conditions)
|
||||||
|
where_parts.append(f'({or_clause})')
|
||||||
|
where_values.extend([f'%{keyword}%'] * len(text_cols))
|
||||||
|
|
||||||
|
where_clause = (' WHERE ' + ' AND '.join(where_parts)) if where_parts else ''
|
||||||
|
|
||||||
|
if where_clause:
|
||||||
|
cursor.execute(f'SELECT COUNT(*) FROM `{table_name}`{where_clause}', where_values)
|
||||||
|
total = cursor.fetchone()[0]
|
||||||
|
else:
|
||||||
|
cursor.execute(f'SELECT COUNT(*) FROM `{table_name}`')
|
||||||
|
total = cursor.fetchone()[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
page = max(int(params.get('PageIndex', 1)), 1)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
page = 1
|
||||||
|
try:
|
||||||
|
page_size = min(max(int(params.get('PageSize', 20)), 1), 200)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
page_size = 20
|
||||||
|
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
cursor.execute(
|
||||||
|
f'SELECT * FROM `{table_name}`{where_clause} ORDER BY 1 LIMIT %s OFFSET %s',
|
||||||
|
where_values + [page_size, offset]
|
||||||
|
)
|
||||||
|
desc = [d[0] for d in cursor.description]
|
||||||
|
records = []
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
record = {}
|
||||||
|
for i, val in enumerate(row):
|
||||||
|
val = self._SerializeValue(val)
|
||||||
|
record[desc[i]] = val
|
||||||
|
records.append(record)
|
||||||
|
|
||||||
|
return {'records': records, 'total': total, 'page': page, 'PageSize': page_size}
|
||||||
|
|
||||||
|
def _AddRecord(self, cursor, table_name, record):
|
||||||
|
"""新增记录"""
|
||||||
|
if not self._ValidateTableName(cursor, table_name):
|
||||||
|
return {'code': 1, 'msg': f'表 {table_name} 不存在'}
|
||||||
|
|
||||||
|
col_info = self._GetTableColumns(cursor, table_name)
|
||||||
|
|
||||||
|
fields = []
|
||||||
|
values = []
|
||||||
|
for key, val in record.items():
|
||||||
|
if key not in col_info:
|
||||||
|
continue
|
||||||
|
if col_info[key]['is_auto']:
|
||||||
|
continue
|
||||||
|
fields.append(f'`{key}`')
|
||||||
|
values.append(self._ConvertValueForDB(val, col_info[key]))
|
||||||
|
|
||||||
|
if not fields:
|
||||||
|
return {'code': 1, 'msg': '没有可插入的字段'}
|
||||||
|
|
||||||
|
placeholders = ', '.join(['%s'] * len(fields))
|
||||||
|
sql = f'INSERT INTO `{table_name}` ({", ".join(fields)}) VALUES ({placeholders})'
|
||||||
|
cursor.execute(sql, values)
|
||||||
|
return {'code': 0, 'msg': '新增成功'}
|
||||||
|
|
||||||
|
def _ModifyRecord(self, cursor, table_name, record):
|
||||||
|
"""修改记录"""
|
||||||
|
if not self._ValidateTableName(cursor, table_name):
|
||||||
|
return {'code': 1, 'msg': f'表 {table_name} 不存在'}
|
||||||
|
|
||||||
|
col_info = self._GetTableColumns(cursor, table_name)
|
||||||
|
pk_cols = {k: v for k, v in col_info.items() if v['is_pk']}
|
||||||
|
|
||||||
|
if not pk_cols:
|
||||||
|
return {'code': 1, 'msg': '表没有主键,无法定位记录'}
|
||||||
|
|
||||||
|
set_parts = []
|
||||||
|
set_values = []
|
||||||
|
for key, val in record.items():
|
||||||
|
if key not in col_info or key in pk_cols:
|
||||||
|
continue
|
||||||
|
set_parts.append(f'`{key}` = %s')
|
||||||
|
set_values.append(self._ConvertValueForDB(val, col_info[key]))
|
||||||
|
|
||||||
|
if not set_parts:
|
||||||
|
return {'code': 0, 'msg': '没有需要更新的字段'}
|
||||||
|
|
||||||
|
where_parts = []
|
||||||
|
where_values = []
|
||||||
|
for pk_name in pk_cols:
|
||||||
|
if pk_name not in record:
|
||||||
|
return {'code': 1, 'msg': f'缺少主键字段: {pk_name}'}
|
||||||
|
where_parts.append(f'`{pk_name}` = %s')
|
||||||
|
where_values.append(self._ConvertValueForDB(record[pk_name], pk_cols[pk_name]))
|
||||||
|
|
||||||
|
sql = f'UPDATE `{table_name}` SET {", ".join(set_parts)} WHERE {" AND ".join(where_parts)}'
|
||||||
|
cursor.execute(sql, set_values + where_values)
|
||||||
|
|
||||||
|
if cursor.rowcount == 0:
|
||||||
|
return {'code': 1, 'msg': '记录不存在或未变更'}
|
||||||
|
return {'code': 0, 'msg': '修改成功'}
|
||||||
|
|
||||||
|
def _DeleteRecord(self, cursor, table_name, record):
|
||||||
|
"""删除记录"""
|
||||||
|
if not self._ValidateTableName(cursor, table_name):
|
||||||
|
return {'code': 1, 'msg': f'表 {table_name} 不存在'}
|
||||||
|
|
||||||
|
col_info = self._GetTableColumns(cursor, table_name)
|
||||||
|
pk_cols = {k: v for k, v in col_info.items() if v['is_pk']}
|
||||||
|
|
||||||
|
if not pk_cols:
|
||||||
|
return {'code': 1, 'msg': '表没有主键,无法定位记录'}
|
||||||
|
|
||||||
|
where_parts = []
|
||||||
|
where_values = []
|
||||||
|
for pk_name in pk_cols:
|
||||||
|
if pk_name not in record:
|
||||||
|
return {'code': 1, 'msg': f'缺少主键字段: {pk_name}'}
|
||||||
|
where_parts.append(f'`{pk_name}` = %s')
|
||||||
|
where_values.append(self._ConvertValueForDB(record[pk_name], pk_cols[pk_name]))
|
||||||
|
|
||||||
|
sql = f'DELETE FROM `{table_name}` WHERE {" AND ".join(where_parts)}'
|
||||||
|
cursor.execute(sql, where_values)
|
||||||
|
|
||||||
|
if cursor.rowcount == 0:
|
||||||
|
return {'code': 1, 'msg': '记录不存在'}
|
||||||
|
return {'code': 0, 'msg': '删除成功'}
|
||||||
|
|
||||||
|
# ─── 工具方法 ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _ValidateTableName(self, cursor, table_name):
|
||||||
|
"""验证表名存在"""
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT COUNT(*) FROM information_schema.TABLES "
|
||||||
|
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s",
|
||||||
|
[table_name]
|
||||||
|
)
|
||||||
|
return cursor.fetchone()[0] > 0
|
||||||
|
|
||||||
|
def _GetTableColumns(self, cursor, table_name):
|
||||||
|
"""获取表的列信息"""
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, COLUMN_KEY, EXTRA, IS_NULLABLE "
|
||||||
|
"FROM information_schema.COLUMNS "
|
||||||
|
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s "
|
||||||
|
"ORDER BY ORDINAL_POSITION",
|
||||||
|
[table_name]
|
||||||
|
)
|
||||||
|
cols = {}
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
col_name, data_type, column_type, col_key, extra, is_nullable = row
|
||||||
|
cols[col_name] = {
|
||||||
|
'type': data_type,
|
||||||
|
'column_type': column_type or '',
|
||||||
|
'is_pk': col_key == 'PRI',
|
||||||
|
'is_auto': 'auto_increment' in (extra or '').lower(),
|
||||||
|
'is_nullable': is_nullable == 'YES',
|
||||||
|
}
|
||||||
|
return cols
|
||||||
|
|
||||||
|
def _ConvertValueForDB(self, value, col_info):
|
||||||
|
"""将前端传来的值转换为数据库可接受的类型"""
|
||||||
|
if value is None or value == '':
|
||||||
|
if col_info['is_nullable']:
|
||||||
|
return None
|
||||||
|
return ''
|
||||||
|
|
||||||
|
data_type = col_info['type']
|
||||||
|
|
||||||
|
if data_type in ('int', 'bigint', 'tinyint', 'smallint', 'mediumint'):
|
||||||
|
return int(value)
|
||||||
|
elif data_type in ('float', 'double', 'decimal'):
|
||||||
|
return float(value)
|
||||||
|
elif data_type in ('binary', 'varbinary'):
|
||||||
|
import re
|
||||||
|
hex_str = re.sub(r'[^0-9a-fA-F]', '', str(value))
|
||||||
|
if len(hex_str) == 32:
|
||||||
|
return uuid.UUID(hex_str).bytes
|
||||||
|
return value
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _SerializeValue(self, val):
|
||||||
|
"""将数据库值序列化为 JSON 安全格式"""
|
||||||
|
if isinstance(val, bytes):
|
||||||
|
if len(val) == 16:
|
||||||
|
try:
|
||||||
|
return str(uuid.UUID(bytes=val))
|
||||||
|
except ValueError:
|
||||||
|
return val.hex()
|
||||||
|
return val.hex()
|
||||||
|
elif isinstance(val, datetime.datetime):
|
||||||
|
return val.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
elif isinstance(val, datetime.date):
|
||||||
|
return val.isoformat()
|
||||||
|
elif isinstance(val, datetime.time):
|
||||||
|
return val.strftime('%H:%M:%S')
|
||||||
|
elif isinstance(val, (bytearray, memoryview)):
|
||||||
|
return bytes(val).hex()
|
||||||
|
return val
|
||||||
|
|
||||||
|
def _FindModelVerboseNames(self, table_name):
|
||||||
|
"""从 Django 模型中获取字段的 verbose_name"""
|
||||||
|
from django.apps import apps
|
||||||
|
verbose_map = {}
|
||||||
|
try:
|
||||||
|
for model in apps.get_models():
|
||||||
|
if model._meta.db_table == table_name:
|
||||||
|
logger.info(f'[ORM] _FindModelVerboseNames matched model={model.__name__} db_table={model._meta.db_table}')
|
||||||
|
for f in model._meta.get_fields():
|
||||||
|
# 获取数据库列名
|
||||||
|
col_name = None
|
||||||
|
if hasattr(f, 'column') and f.column:
|
||||||
|
col_name = f.column
|
||||||
|
elif hasattr(f, 'db_column') and f.db_column:
|
||||||
|
col_name = f.db_column
|
||||||
|
elif hasattr(f, 'attname'):
|
||||||
|
col_name = f.attname
|
||||||
|
|
||||||
|
if not col_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
vn = getattr(f, 'verbose_name', None)
|
||||||
|
if vn and str(vn) != col_name:
|
||||||
|
verbose_map[col_name] = str(vn)
|
||||||
|
logger.debug(f'[ORM] {col_name} -> {str(vn)}')
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# 没有匹配的模型
|
||||||
|
available = [m._meta.db_table for m in apps.get_models()]
|
||||||
|
logger.warning(f'[ORM] _FindModelVerboseNames: 未找到 db_table={table_name} 的模型, 可用表: {available[:20]}...')
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f'[ORM] _FindModelVerboseNames 异常: {e}')
|
||||||
|
return verbose_map
|
||||||
0
jituan/__init__.py
Normal file
0
jituan/__init__.py
Normal file
7
jituan/apps.py
Normal file
7
jituan/apps.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class JituanConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'jituan'
|
||||||
|
verbose_name = '集团多俱乐部'
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user