import random from decimal import Decimal from django.conf import settings from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand, CommandError from django.db import transaction from basic_info import models as basic_models from printing import models as printing_models from shipment import models as shipment_models class Command(BaseCommand): help = ( "为开发环境生成一批假的 customer / printing_order / printing_job / sales_item 数据," "用于前端联调 shipment 与 printing 相关页面。" ) def add_arguments(self, parser): parser.add_argument("--merchant-id", type=int, default=1, help="目标商户 ID,默认 1") parser.add_argument("--user-id", type=int, default=None, help="创建人用户 ID,可选") parser.add_argument("--customers", type=int, default=10, help="客户数量,默认 10") parser.add_argument("--orders", type=int, default=50, help="订单数量,默认 50") parser.add_argument( "--jobs-min", type=int, default=3, help="每个订单最少生成多少个 PrintingJob,默认 3" ) parser.add_argument( "--jobs-max", type=int, default=30, help="每个订单最多生成多少个 PrintingJob,默认 30" ) parser.add_argument( "--items-min", type=int, default=2, help="每个 job 最少生成多少个 SalesItem,默认 2" ) parser.add_argument( "--items-max", type=int, default=20, help="每个 job 最多生成多少个 SalesItem,默认 20" ) parser.add_argument( "--products", type=int, default=24, help="测试产品数量,默认 24" ) parser.add_argument( "--tag", type=str, default="DEVSHIP", help="生成数据的标签前缀,默认 DEVSHIP", ) parser.add_argument( "--seed", type=int, default=20260331, help="随机种子,默认 20260331", ) parser.add_argument( "--yes", action="store_true", help="跳过交互确认,直接执行(谨慎使用)", ) parser.add_argument( "--dry-run", action="store_true", help="仅输出执行前检查与预计写入量,不实际写入数据", ) def handle(self, *args, **options): random.seed(options["seed"]) self._validate_options(options) merchant = basic_models.Merchant.objects.filter(id=options["merchant_id"]).first() if merchant is None: raise CommandError(f"merchant_id={options['merchant_id']} 不存在") created_by = self._resolve_user(merchant=merchant, user_id=options["user_id"]) preview = self._collect_preview(merchant=merchant, created_by=created_by, options=options) self._print_preview(preview) if options["dry_run"]: self.stdout.write(self.style.WARNING("dry-run 模式:未写入任何数据")) return if not options["yes"]: self._confirm_or_abort() with transaction.atomic(): counts = self._generate_data( merchant=merchant, created_by=created_by, options=options, ) self._print_summary( merchant=merchant, created_by=created_by, tag=options["tag"], counts=counts, ) def _generate_data(self, *, merchant, created_by, options): category = self._ensure_category(merchant=merchant, tag=options["tag"]) products = self._ensure_products( merchant=merchant, category=category, count=options["products"], tag=options["tag"], ) customers = self._create_customers( merchant=merchant, count=options["customers"], tag=options["tag"], ) order_count = 0 job_count = 0 sales_item_count = 0 self.stdout.write(self.style.NOTICE("开始生成测试数据...")) self.stdout.write("步骤 1/3: 准备客户与产品") self.stdout.write(f" 客户数: {len(customers)}") self.stdout.write(f" 产品数: {len(products)}") self.stdout.write("步骤 2/3: 创建 PrintingOrder / PrintingJob / SalesItem") for index in range(1, options["orders"] + 1): customer = customers[(index - 1) % len(customers)] order = printing_models.PrintingOrder.objects.create( merchant=merchant, customer=customer, fabric=random.choice(["全棉", "涤纶", "尼龙", "人棉", "TC"]), width=random.choice(["150cm", "160cm", "170cm", "180cm"]), is_urgent=random.choice([False, False, False, True]), area=customer.area or random.choice(["广州", "佛山", "绍兴", "杭州"]), address=f"{customer.area or '广州'}-{index}号测试地址", fabric_source=random.choice(["自带布", "仓库", "客户送布"]), is_fabric_received=random.choice([True, False]), craft=random.choice(["活性印花", "数码印花", "涂料印花"]), description=f"{options['tag']} 测试印染订单 #{index}", position=random.choice(["A区", "B区", "C区", "待排产"]), printing_warn=random.choice(["", "注意色差", "先确认花型"]), rolling_warn=random.choice(["", "注意卷边", "慢速滚筒"]), production_warn=random.choice(["", "先打样", "优先安排"]), created_by=created_by, external_order_id=self._build_external_order_id(index=index), external_customer_id=f"CUST-{customer.id}", external_customer_name=customer.name, external_employee_name=self._display_user(created_by), ) order_count += 1 jobs_for_order = random.randint(options["jobs_min"], options["jobs_max"]) for job_index in range(1, jobs_for_order + 1): product = random.choice(products) job = printing_models.PrintingJob.objects.create( merchant=merchant, printing_order=order, product=product, work_state=random.choice( [ printing_models.PrintingJobWorkStateEnum.PRODUCING, printing_models.PrintingJobWorkStateEnum.WAITING_FOR_DELIVERY, printing_models.PrintingJobWorkStateEnum.WAITING_FOR_INVOICE, ] ), quantity=random.randint(80, 3000), unit=random.choice(["米", "码", "件"]), size=random.choice(["", "120x120", "150x150", "200x200"]) or None, pieces=random.choice([None, 1, 2, 4, 6, 8]), description=f"{options['tag']} Job {index}-{job_index}", created_by=created_by, external_product_name=product.name, ) job_count += 1 sales_items = [] items_for_job = random.randint(options["items_min"], options["items_max"]) for item_index in range(1, items_for_job + 1): sales_items.append( shipment_models.SalesItem( shipment=None, merchant=merchant, name=f"{product.name}-销售品-{index}-{job_index}-{item_index}", quantity=Decimal( f"{random.randint(20, 3000)}.{random.randint(0, 99):02d}" ), unit=random.choice( [ shipment_models.UnitChoices.METER, shipment_models.UnitChoices.PIECE, shipment_models.UnitChoices.YARD, shipment_models.UnitChoices.UNIT, ] ), created_by=created_by, printing_job_id=job.id, customer_id=customer.id, position=random.choice( [ "A1-01", "A1-02", "B2-03", "C3-05", "待分区", "待发货区", ] ), remark=random.choice(["", "测试数据", "扫码联调用", "优先出货"]), ) ) shipment_models.SalesItem.objects.bulk_create(sales_items, batch_size=500) sales_item_count += len(sales_items) if index % 10 == 0 or index == options["orders"]: self.stdout.write( f" 已完成订单 {index}/{options['orders']}," f"累计 jobs={job_count},sales_items={sales_item_count}" ) self.stdout.write("步骤 3/3: 数据写入完成") return { "customers_created": len(customers), "printing_orders_created": order_count, "printing_jobs_created": job_count, "sales_items_created": sales_item_count, } def _resolve_user(self, *, merchant, user_id): User = get_user_model() if user_id is not None: user = User.objects.filter(id=user_id).first() if user is None: raise CommandError(f"user_id={user_id} 不存在") employee = getattr(user, "employee", None) if employee is None or employee.merchant_id != merchant.id: raise CommandError("指定 user 未绑定到目标商户的 employee") return user employee = ( basic_models.Employee.objects.select_related("sys_user") .filter(merchant=merchant, sys_user__isnull=False) .order_by("id") .first() ) if employee and employee.sys_user: return employee.sys_user username = f"seed_{merchant.id}_user" user = User.objects.create_user(username=username, password="devpass123") basic_models.Employee.objects.create( merchant=merchant, sys_user=user, name=f"Seed User {merchant.id}", mobile=f"1390000{merchant.id:04d}", status=basic_models.EmployeeStatusEnum.ACTIVE, ) return user def _ensure_category(self, *, merchant, tag): category, _ = basic_models.ProductCategory.objects.get_or_create( merchant=merchant, name=f"{tag}-测试分类", defaults={"product_prefix": "TS"}, ) return category def _ensure_products(self, *, merchant, category, count, tag): products = list( basic_models.Product.objects.filter( merchant=merchant, category=category, name__startswith=f"{tag}-测试产品-", ).order_by("id") ) missing = count - len(products) for index in range(1, missing + 1): number = len(products) + index products.append( basic_models.Product.objects.create( merchant=merchant, category=category, name=f"{tag}-测试产品-{number:03d}", human_id=f"{tag[:4]}P{number:03d}", unit=random.choice( [ basic_models.ProductUnitEnum.METER, basic_models.ProductUnitEnum.YARD, basic_models.ProductUnitEnum.SEGMENT, ] ), color=random.choice(["红", "蓝", "黑", "白", "灰"]), width_size=Decimal(random.choice(["150.00", "160.00", "170.00"])), description="开发环境自动生成的测试产品", from_mdy=False, ) ) return list( basic_models.Product.objects.filter( merchant=merchant, category=category, name__startswith=f"{tag}-测试产品-", ).order_by("id")[:count] ) def _create_customers(self, *, merchant, count, tag): customers = [] employee = ( basic_models.Employee.objects.filter(merchant=merchant).order_by("id").first() ) if employee is None: raise CommandError("目标商户下没有 employee,无法创建 customer") for index in range(1, count + 1): name = f"{tag}-测试客户-{index:02d}" customer, _ = basic_models.Customer.objects.get_or_create( merchant=merchant, name=name, defaults={ "created_by": employee, "mobile": f"138{merchant.id:02d}{index:06d}"[:11], "area": random.choice(["广州", "佛山", "绍兴", "杭州", "苏州"]), "contact": f"联系人{index:02d}", "description": "开发环境自动生成的测试客户", }, ) customers.append(customer) return customers def _build_external_order_id(self, *, index): return f"KD{20135142 + index:08d}" def _validate_options(self, options): if options["customers"] <= 0: raise CommandError("--customers 必须大于 0") if options["orders"] <= 0: raise CommandError("--orders 必须大于 0") if options["products"] <= 0: raise CommandError("--products 必须大于 0") if options["jobs_min"] <= 0 or options["jobs_max"] <= 0: raise CommandError("--jobs-min / --jobs-max 必须大于 0") if options["items_min"] <= 0 or options["items_max"] <= 0: raise CommandError("--items-min / --items-max 必须大于 0") if options["jobs_min"] > options["jobs_max"]: raise CommandError("--jobs-min 不能大于 --jobs-max") if options["items_min"] > options["items_max"]: raise CommandError("--items-min 不能大于 --items-max") def _collect_preview(self, *, merchant, created_by, options): db = settings.DATABASES["default"] tag = options["tag"] existing_customer_count = basic_models.Customer.objects.filter( merchant=merchant, name__startswith=f"{tag}-测试客户-", ).count() existing_order_count = printing_models.PrintingOrder.objects.filter( merchant=merchant, description__startswith=f"{tag} 测试印染订单 #", ).count() existing_job_count = printing_models.PrintingJob.objects.filter( merchant=merchant, description__startswith=f"{tag} Job ", ).count() existing_sales_item_count = shipment_models.SalesItem.objects.filter( merchant=merchant, name__startswith=f"{tag}-测试产品-", ).count() jobs_min_total = options["orders"] * options["jobs_min"] jobs_max_total = options["orders"] * options["jobs_max"] sales_items_min_total = jobs_min_total * options["items_min"] sales_items_max_total = jobs_max_total * options["items_max"] return { "debug": getattr(settings, "DEBUG", None), "db_host": db.get("HOST"), "db_port": db.get("PORT"), "db_name": db.get("NAME"), "merchant_id": merchant.id, "merchant_name": merchant.name, "created_by_id": created_by.id, "created_by_username": created_by.username, "tag": tag, "seed": options["seed"], "customers": options["customers"], "orders": options["orders"], "jobs_min": options["jobs_min"], "jobs_max": options["jobs_max"], "items_min": options["items_min"], "items_max": options["items_max"], "products": options["products"], "existing_customer_count": existing_customer_count, "existing_order_count": existing_order_count, "existing_job_count": existing_job_count, "existing_sales_item_count": existing_sales_item_count, "jobs_min_total": jobs_min_total, "jobs_max_total": jobs_max_total, "sales_items_min_total": sales_items_min_total, "sales_items_max_total": sales_items_max_total, } def _print_preview(self, preview): self.stdout.write(self.style.WARNING("即将生成开发环境测试数据,请先确认以下状态")) self.stdout.write("=" * 72) self.stdout.write(f"DEBUG : {preview['debug']}") self.stdout.write( f"数据库 : {preview['db_name']} @ {preview['db_host']}:{preview['db_port']}" ) self.stdout.write( f"目标商户 : {preview['merchant_id']} / {preview['merchant_name']}" ) self.stdout.write( f"创建人用户 : {preview['created_by_id']} / {preview['created_by_username']}" ) self.stdout.write(f"数据标签 : {preview['tag']}") self.stdout.write(f"随机种子 : {preview['seed']}") self.stdout.write("-" * 72) self.stdout.write(f"客户数 : {preview['customers']}") self.stdout.write(f"订单数 : {preview['orders']}") self.stdout.write( f"每单 job 数 : {preview['jobs_min']} ~ {preview['jobs_max']}" ) self.stdout.write( f"每 job sales item 数: {preview['items_min']} ~ {preview['items_max']}" ) self.stdout.write(f"测试产品数 : {preview['products']}") self.stdout.write("-" * 72) self.stdout.write( f"预计新增 PrintingJob: {preview['jobs_min_total']} ~ {preview['jobs_max_total']}" ) self.stdout.write( "预计新增 SalesItem : " f"{preview['sales_items_min_total']} ~ {preview['sales_items_max_total']}" ) self.stdout.write("-" * 72) self.stdout.write( f"当前同 tag 客户数 : {preview['existing_customer_count']}" ) self.stdout.write( f"当前同 tag 订单数 : {preview['existing_order_count']}" ) self.stdout.write( f"当前同 tag job 数 : {preview['existing_job_count']}" ) self.stdout.write( f"当前同 tag sales item: {preview['existing_sales_item_count']}" ) self.stdout.write("=" * 72) def _confirm_or_abort(self): answer = input("确认要继续写入这批测试数据吗?请输入 YES 继续: ").strip() if answer != "YES": raise CommandError("已取消执行,未写入任何数据") def _print_summary(self, *, merchant, created_by, tag, counts): self.stdout.write(self.style.SUCCESS("本次执行摘要")) self.stdout.write("=" * 72) self.stdout.write(f"merchant_id : {merchant.id}") self.stdout.write(f"merchant_name : {merchant.name}") self.stdout.write(f"created_by : {created_by.id}:{created_by.username}") self.stdout.write(f"tag : {tag}") self.stdout.write(f"customers_created : {counts['customers_created']}") self.stdout.write(f"printing_orders : {counts['printing_orders_created']}") self.stdout.write(f"printing_jobs : {counts['printing_jobs_created']}") self.stdout.write(f"sales_items : {counts['sales_items_created']}") self.stdout.write("=" * 72) self.stdout.write(self.style.SUCCESS("假数据生成完成")) def _display_user(self, user): employee = getattr(user, "employee", None) if employee: return employee.name return user.username