Skip to content

Commit 4c335bc

Browse files
committed
feat(birthboard): 通知发件人配置化、审核提醒开关与后台管理增强
- 通知发件人改由 config.birthboard.sender_username/sender_name 控制, notify.py 统一改用 record.log logger,避免发件人缺失时静默失败; 新增 ensure_birthboard_sender 幂等创建官方组织账号的命令。 - 一审/二审员新增 reminder_enabled 开关:关闭后仍可审核,但不再接收 自动重复审核提醒(notify_approval_reminder 只发给开启者)。 - 投放记录接入 admin:只读流程字段 + 参与者/变更记录只读内联,可改 date; 审核员/二审员管理页提供内联开关。 - 四个模型补充中文 verbose_name 并用序号前缀控制 admin 显示顺序。 - config_template.json:birthboard 段新增 sender_username/sender_name; scheduler.use_scheduler 默认改为 true(新部署默认启用调度集成)。 Files changed: - birthboard/config.py: 新增 sender_username/sender_name 配置项。 - birthboard/notify.py: 发件人读取 config(_sender_user),logger 换统一 logger;审核提醒按 reminder_enabled 过滤。 - birthboard/management/commands/ensure_birthboard_sender.py: 新增幂等创建发件人命令。 - birthboard/models.py: approver/second approver 新增 reminder_enabled;四个模型加中文 verbose_name。 - birthboard/admin.py: 审核员内联开关;投放记录只读 admin 与参与者/变更记录只读内联。 - birthboard/migrations/0015_*: 新增 reminder_enabled 字段。 - birthboard/migrations/0016_*: 模型 Meta 选项(verbose_name)变更。 - birthboard/tests.py: 通知发件人与提醒开关回归测试。 - config_template.json: birthboard 段新增发件人配置;scheduler.use_scheduler 默认 true。
1 parent 1c5da59 commit 4c335bc

9 files changed

Lines changed: 404 additions & 26 deletions

birthboard/admin.py

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,119 @@
11
from django.contrib import admin
2+
from django.utils.html import format_html
23
from birthboard.models import (
34
BirthboardApprover,
45
BirthboardContract,
6+
BirthboardParticipant,
7+
BirthboardRecord,
58
BirthboardSecondApprover,
9+
ChangeRecord,
610
)
711

812
@admin.register(BirthboardApprover)
913
class BirthboardApproverAdmin(admin.ModelAdmin):
10-
list_display = ("user", "is_active", "created_at", "note")
14+
# is_active:是否允许该审核员参与审核;reminder_enabled:是否接收自动重复审核提醒
15+
# 两者独立,均可在列表内直接勾选/取消,保存即生效。
16+
list_display = ("user", "is_active", "reminder_enabled", "created_at", "note")
17+
list_editable = ("is_active", "reminder_enabled")
1118
search_fields = ("user__username", "user__first_name", "user__last_name")
12-
list_filter = ("is_active",)
19+
list_filter = ("is_active", "reminder_enabled")
1320
autocomplete_fields = ["user"]
1421

1522
@admin.register(BirthboardSecondApprover)
1623
class BirthboardSecondApproverAdmin(admin.ModelAdmin):
17-
list_display = ("user", "is_active", "created_at", "note")
24+
list_display = ("user", "is_active", "reminder_enabled", "created_at", "note")
25+
list_editable = ("is_active", "reminder_enabled")
1826
search_fields = ("user__username", "user__first_name", "user__last_name")
19-
list_filter = ("is_active",)
27+
list_filter = ("is_active", "reminder_enabled")
2028
autocomplete_fields = ["user"]
2129

2230

31+
class _ReadOnlyInline(admin.TabularInline):
32+
"""只读内联基类:子记录仅用于查看,禁止新增/删除/编辑。"""
33+
extra = 0
34+
can_delete = False
35+
36+
def has_add_permission(self, request, obj=None):
37+
return False
38+
39+
def has_change_permission(self, request, obj=None):
40+
return False
41+
42+
def has_delete_permission(self, request, obj=None):
43+
return False
44+
45+
46+
class BirthboardParticipantInline(_ReadOnlyInline):
47+
"""投放参与者明细(送出人/寿星、角色、扣款状态等)。"""
48+
model = BirthboardParticipant
49+
fields = (
50+
"user", "role", "is_initiator", "cost", "status", "action_time",
51+
)
52+
53+
54+
class ChangeRecordInline(_ReadOnlyInline):
55+
"""投放变更审计记录(操作、前后状态、详情等)。"""
56+
model = ChangeRecord
57+
# created_at 为 auto_now_add(不可编辑),只能经 readonly_fields 展示
58+
fields = (
59+
"actor", "action", "before_status", "after_status", "detail",
60+
)
61+
readonly_fields = ("created_at",)
62+
63+
64+
@admin.register(BirthboardRecord)
65+
class BirthboardRecordAdmin(admin.ModelAdmin):
66+
"""投放记录后台:以查看/检索为主,流程字段只读。
67+
68+
状态、金额、审批与投放同步等字段的变更涉及退款、通知与屏幕同步,必须走
69+
业务入口(页面/任务),禁止在 admin 直接修改,避免绕过事务与审计。
70+
记录由业务流程创建,admin 不提供新增。
71+
"""
72+
list_display = (
73+
"id",
74+
"receiver_name",
75+
"receiver_username",
76+
"date",
77+
"status",
78+
"per_cost",
79+
"is_anonymous",
80+
"thumbnail_preview",
81+
"created_at",
82+
)
83+
list_display_links = ("id", "receiver_name")
84+
list_filter = ("status", "date", "is_anonymous")
85+
search_fields = ("receiver_name", "receiver_username", "id")
86+
date_hierarchy = "date"
87+
# 详情页同时内联展示参与者明细与变更审计记录(只读)
88+
inlines = (BirthboardParticipantInline, ChangeRecordInline)
89+
readonly_fields = (
90+
"receiver_username",
91+
"mode",
92+
"per_cost",
93+
"status",
94+
"created_at",
95+
"first_approved",
96+
"first_approver",
97+
"first_approved_at",
98+
"second_approver",
99+
"second_approved_at",
100+
"display_takedown_pending",
101+
)
102+
103+
@admin.display(description="缩略图")
104+
def thumbnail_preview(self, obj):
105+
"""列表里直接预览海报缩略图,便于核对内容。"""
106+
if not obj.thumbnail:
107+
return "—"
108+
return format_html(
109+
'<img src="{}" height="40" style="border-radius:4px" />',
110+
obj.thumbnail.url,
111+
)
112+
113+
def has_add_permission(self, request):
114+
return False
115+
116+
23117
@admin.register(BirthboardContract)
24118
class BirthboardContractAdmin(admin.ModelAdmin):
25119
list_display = ("user", "signed", "signed_at", "restricted_until")

birthboard/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ class BirthboardConfig(Config):
2121
type=int,
2222
)
2323
max_senders = LazySetting('max_senders', default=20, type=int)
24+
# 站内信/企业微信通知的发件人官方组织账号(Organization 类型)。
25+
# 用管理命令 `python manage.py ensure_birthboard_sender` 幂等创建;
26+
# 切换发件人只需改这里的 username(指向真实存在的组织账号)。
27+
sender_username = LazySetting(
28+
'sender_username',
29+
default='yppf_birthboard',
30+
type=str,
31+
)
32+
# 该发件人组织的显示名(Organization.oname / User.name),供创建命令使用。
33+
sender_name = LazySetting(
34+
'sender_name',
35+
default='生日灯牌',
36+
type=str,
37+
)
2438

2539

2640
CONFIG = BirthboardConfig(ROOT_CONFIG.get('birthboard', {}))
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""幂等创建 birthboard 官方发送组织账号(站内信/企业微信通知的发件人)。
2+
3+
发件人账号与组织名由 birthboard config 决定:
4+
config.birthboard.sender_username(默认 yppf_birthboard)
5+
config.birthboard.sender_name(默认 生日灯牌)
6+
改 config.json 后重新运行本命令即可切换/补齐账号。
7+
8+
用法:
9+
python manage.py ensure_birthboard_sender
10+
"""
11+
from django.core.management.base import BaseCommand
12+
13+
from generic.models import User
14+
from app.models import Organization, OrganizationType
15+
16+
from birthboard.config import CONFIG
17+
18+
19+
class Command(BaseCommand):
20+
help = 'Ensure the birthboard official sender org account exists (config-driven).'
21+
22+
def handle(self, *args, **options):
23+
username = CONFIG.sender_username
24+
name = CONFIG.sender_name
25+
if not username or not name:
26+
self.stderr.write(self.style.ERROR(
27+
'birthboard.sender_username / sender_name 未配置,无法创建发送账号。'))
28+
return
29+
30+
# 1) 确保账号(User,Organization 类型)存在
31+
user = User.objects.filter(username=username).first()
32+
if user is None:
33+
user = User.objects.create_user(
34+
username=username,
35+
name=name,
36+
usertype=User.Type.ORG,
37+
active=True,
38+
is_active=True,
39+
is_newuser=False,
40+
)
41+
self.stdout.write(self.style.SUCCESS(
42+
f'创建发送账号:{username}{name})'))
43+
else:
44+
updates = {}
45+
if user.name != name:
46+
updates['name'] = name
47+
if user.utype != User.Type.ORG:
48+
updates['utype'] = User.Type.ORG
49+
if not user.active:
50+
updates['active'] = True
51+
if not user.is_active:
52+
updates['is_active'] = True
53+
if user.is_newuser:
54+
updates['is_newuser'] = False
55+
if updates:
56+
User.objects.filter(pk=user.pk).update(**updates)
57+
self.stdout.write(self.style.WARNING(
58+
f'更新发送账号字段:{", ".join(sorted(updates))}'))
59+
60+
# 2) 确保账号对应的组织资料(Organization 一对一)存在
61+
org = Organization.objects.filter(organization_id=user).first()
62+
if org is not None:
63+
if org.oname != name:
64+
self.stdout.write(self.style.WARNING(
65+
f'组织资料已存在(oname={org.oname}),与 sender_name={name!r} 不一致,'
66+
'未自动改名,请按需在 admin 调整。'))
67+
return
68+
# 组织名唯一,先检查是否被其它账号占用
69+
org_by_name = Organization.objects.filter(oname=name).first()
70+
if org_by_name is not None:
71+
self.stderr.write(self.style.ERROR(
72+
f'组织名 {name!r} 已被其他账号({org_by_name.organization_id.username})'
73+
'占用,请调整 birthboard.sender_name。'))
74+
return
75+
otype = OrganizationType.objects.order_by('otype_id').first()
76+
if otype is None:
77+
self.stderr.write(self.style.ERROR(
78+
'数据库中不存在 OrganizationType,无法为发送账号创建组织资料。'))
79+
return
80+
Organization.objects.create(
81+
organization_id=user,
82+
oname=name,
83+
otype=otype,
84+
)
85+
self.stdout.write(self.style.SUCCESS(
86+
f'创建组织资料:{name}(账号 {username})'))
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.2.13 on 2026-09-04 23:31
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('birthboard', '0014_harden_review_and_takedown'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='birthboardapprover',
15+
name='reminder_enabled',
16+
field=models.BooleanField(default=True, verbose_name='接收重复提醒'),
17+
),
18+
migrations.AddField(
19+
model_name='birthboardsecondapprover',
20+
name='reminder_enabled',
21+
field=models.BooleanField(default=True, verbose_name='接收重复提醒'),
22+
),
23+
]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 5.2.13 on 2026-09-04 23:49
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('birthboard', '0015_birthboardapprover_reminder_enabled_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AlterModelOptions(
14+
name='birthboardapprover',
15+
options={'verbose_name': '初审人员', 'verbose_name_plural': '1.初审人员'},
16+
),
17+
migrations.AlterModelOptions(
18+
name='birthboardcontract',
19+
options={'verbose_name': '协议记录', 'verbose_name_plural': '3.协议记录与小黑屋'},
20+
),
21+
migrations.AlterModelOptions(
22+
name='birthboardrecord',
23+
options={'verbose_name': '灯牌记录', 'verbose_name_plural': '4.灯牌记录'},
24+
),
25+
migrations.AlterModelOptions(
26+
name='birthboardsecondapprover',
27+
options={'verbose_name': '终审人员', 'verbose_name_plural': '2.终审人员'},
28+
),
29+
]

birthboard/models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
# Create your models here.
1818
# 生日祝福投放记录模型,顶格定义
1919
class BirthboardRecord(models.Model):
20+
class Meta:
21+
verbose_name = '灯牌记录'
22+
verbose_name_plural = '4.灯牌记录'
23+
2024
receiver_username = models.CharField(max_length=64)
2125
receiver_name = models.CharField(max_length=64)
2226
date = models.DateField()
@@ -95,8 +99,14 @@ def log(cls, record, actor=None, action='', before_status='', after_status='', d
9599
)
96100
# 二审员名单表
97101
class BirthboardSecondApprover(models.Model):
102+
class Meta:
103+
verbose_name = '终审人员'
104+
verbose_name_plural = '2.终审人员'
105+
98106
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='birthboard_second_approver')
99107
is_active = models.BooleanField(default=True)
108+
# 是否接收自动重复审核提醒;关闭后仍可正常审核,只是不再被提醒打扰
109+
reminder_enabled = models.BooleanField('接收重复提醒', default=True)
100110
created_at = models.DateTimeField(auto_now_add=True)
101111
note = models.CharField(max_length=64, blank=True, default='', help_text='备注')
102112

@@ -153,8 +163,14 @@ def __str__(self):
153163

154164
# 审核员名单表
155165
class BirthboardApprover(models.Model):
166+
class Meta:
167+
verbose_name = '初审人员'
168+
verbose_name_plural = '1.初审人员'
169+
156170
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='birthboard_approver')
157171
is_active = models.BooleanField(default=True)
172+
# 是否接收自动重复审核提醒;关闭后仍可正常审核,只是不再被提醒打扰
173+
reminder_enabled = models.BooleanField('接收重复提醒', default=True)
158174
created_at = models.DateTimeField(auto_now_add=True)
159175
note = models.CharField(max_length=64, blank=True, default='', help_text='备注')
160176

@@ -163,6 +179,10 @@ def __str__(self):
163179

164180
# 用户协议签署记录
165181
class BirthboardContract(models.Model):
182+
class Meta:
183+
verbose_name = '协议记录'
184+
verbose_name_plural = '3.协议记录与小黑屋'
185+
166186
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='birthboard_contract')
167187
signed = models.BooleanField('已签署', default=False)
168188
signed_at = models.DateTimeField('签署时间', null=True, blank=True)

0 commit comments

Comments
 (0)