Files
license-manager/backend/docs/modules_design/2026.1.21-发票模块设计.md
T

15 KiB
Raw Blame History

发票模块设计

1. 背景与目标

客户购买套餐后(订单已支付),需要在用户端发起开票申请;管理员在管理端处理开票、上传发票 PDF,或驳回并给出修改建议;用户可在列表/详情查看处理进度并下载发票文件。

2. 实体关系

  • cu_orders(订单) : invoices(发票) = 1 : 1(一个订单最多一张发票)

约束:

  • invoices.order_id 唯一,确保 1:1

3. 状态与枚举

3.1 开票状态(invoice_status)

  • pending:待处理
  • issued:已开票
  • rejected:已驳回

状态流转:

  • pending -> issued
  • pending -> rejected
  • (可选)rejected -> issued(管理员确认无误后直接开票)

3.2 发票类型(invoice_type)

  • personal:个人
  • enterprise:企业普票
  • vat_special:增值税专用发票

字段约束建议:

  • invoice_type=personal:taxpayer_id 可为空
  • invoice_type=enterprise/vat_special:taxpayer_id 必填

4. 编号与金额口径

4.1 发票申请号(invoice_no)

建议生成规则与订单号风格一致:

  • INV{YYYYMMDD}{9位序列}(序列使用纳秒时间戳后 9 位,确保全局唯一)

4.2 发票金额(amount)

强制以订单 cu_orders.total_amount 为准(下单时已确定),避免用户端传入金额造成口径不一致。

5. 表结构设计(新增 invoices 表)

说明:当前项目大量使用软删除(deleted_at),发票也建议沿用该模式。

5.1 invoices 表(建议)

字段设计(按业务分组):

  • 主键:id
  • 关联:order_id(唯一)、order_no(冗余快照,便于搜索)、customer_id、cu_user_id
  • 金额:amount
  • 业务:status、invoice_type、title、taxpayer_id、content、receiver_email、remark
  • 开票处理:issued_at、invoice_file_url、uploaded_at、uploaded_by
  • 驳回处理:rejected_at、rejected_by、reject_reason、suggestion
  • 下载(可选):download_token(用于邮件链接下载的随机 token)
  • 时间:created_at、updated_at、deleted_at

推荐 SQL(MySQL):

CREATE TABLE `invoices` (
  `id` varchar(36) NOT NULL,
  `invoice_no` varchar(50) NOT NULL COMMENT '发票申请号',
  `order_id` varchar(36) NOT NULL COMMENT '订单ID(唯一)',
  `order_no` varchar(50) NOT NULL COMMENT '订单号(冗余快照,便于检索)',
  `customer_id` varchar(36) NOT NULL COMMENT '客户ID',
  `cu_user_id` varchar(36) NOT NULL COMMENT '申请人(客户用户)ID',

  `amount` decimal(10,2) NOT NULL COMMENT '发票金额(取订单total_amount)',

  `status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/issued/rejected',
  `invoice_type` varchar(20) NOT NULL COMMENT 'personal/enterprise/vat_special',
  `title` varchar(200) NOT NULL COMMENT '发票抬头',
  `taxpayer_id` varchar(50) DEFAULT NULL COMMENT '纳税人识别号',
  `content` varchar(200) NOT NULL COMMENT '开票内容',
  `receiver_email` varchar(255) NOT NULL COMMENT '收票邮箱',
  `remark` varchar(1000) DEFAULT NULL COMMENT '备注',

  `invoice_file_url` varchar(500) DEFAULT NULL COMMENT '发票文件URL(PDF)',
  `uploaded_at` datetime(3) DEFAULT NULL COMMENT '上传时间',
  `uploaded_by` varchar(36) DEFAULT NULL COMMENT '上传人(管理员)ID',
  `issued_at` datetime(3) DEFAULT NULL COMMENT '开票完成时间',

  `reject_reason` varchar(500) DEFAULT NULL COMMENT '驳回原因',
  `suggestion` varchar(500) DEFAULT NULL COMMENT '建议修改内容',
  `rejected_at` datetime(3) DEFAULT NULL COMMENT '驳回时间',
  `rejected_by` varchar(36) DEFAULT NULL COMMENT '驳回人(管理员)ID',

  `download_token` varchar(64) DEFAULT NULL COMMENT '下载token(可选)',

  `created_at` datetime(3) NOT NULL,
  `updated_at` datetime(3) NOT NULL,
  `deleted_at` datetime(3) DEFAULT NULL,

  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_invoices_invoice_no` (`invoice_no`),
  UNIQUE KEY `idx_invoices_order_id` (`order_id`),
  KEY `idx_invoices_customer_id` (`customer_id`),
  KEY `idx_invoices_cu_user_id` (`cu_user_id`),
  KEY `idx_invoices_status` (`status`),
  KEY `idx_invoices_created_at` (`created_at`),
  KEY `idx_invoices_deleted_at` (`deleted_at`),
  KEY `idx_invoices_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

6. 接口设计

6.1 通用约定(与项目一致)

  • 鉴权:客户端 CustomerAuth();管理端 AuthMiddleware()(是否叠加 AdminOnlyMiddleware() 视权限策略)
  • 返回:成功 models.APIResponse,失败 models.ErrorResponse
  • 多语言:Accept-Language 或 ?lang=...

6.2 客户端(/api/cu)

6.2.1 客户申请发票

POST /api/cu/invoices

请求 Body:

{
  "order_id": "order-uuid",
  "invoice_type": "enterprise",
  "receiver_email": "finance@example.com",
  "title": "某某公司",
  "taxpayer_id": "9133xxxxxxxxxxxxxx",
  "content": "软件服务费",
  "remark": "请开电子发票"
}

规则(建议):

  • order_id 必填,且订单必须属于当前用户(cu_orders.cu_user_id)
  • 仅允许已支付订单开票:cu_orders.status=paid
  • 一个订单仅允许一张发票:若已存在发票则返回冲突错误
  • invoice_type 影响 taxpayer_id 是否必填(见 3.2)

成功响应:

{
  "code": "000000",
  "message": "成功",
  "data": { "id": "invoice-uuid", "invoice_no": "INV202601210123456789", "status": "pending" },
  "timestamp": "2026-01-21T10:05:00Z"
}

6.2.2 客户发票列表

GET /api/cu/invoices

Query:

  • page(默认 1)
  • page_size(默认 10,最大 100)
  • status(可选):pending/issued/rejected
  • search(可选):invoice_no 或 order_no 模糊匹配
  • apply_date(可选):申请日期(YYYY-MM-DD,按 created_at 过滤)

6.2.3 客户发票详情

GET /api/cu/invoices/{id}

返回:发票表全部字段 + 关联订单号/套餐类型(可 join cu_orders 返回 order_no/package_name)。

6.2.4 客户发票汇总信息

GET /api/cu/invoices/summary

返回:

  • total_count
  • pending_count
  • issued_count
  • rejected_count

6.2.5 发票下载(推荐:鉴权下载)

GET /api/cu/invoices/{id}/download

仅允许下载自己的发票;返回 PDF 文件流。

6.3 管理端(/api/v1)

6.3.1 管理员发票列表

GET /api/v1/invoices

Query 与客户端一致,额外可支持 customer_id 筛选。

6.3.2 管理员发票详情

GET /api/v1/invoices/{id}

返回:

  • 发票表所有字段
  • 关联订单号、套餐类型(cu_orders.package_name)
  • 申请人姓名/电话(cu_users.real_name/phone)
  • 开票人 full_name(通过 users.full_name,关联 uploaded_by)

6.3.3 管理员发票汇总信息

GET /api/v1/invoices/summary

6.3.4 管理员发票驳回

POST /api/v1/invoices/{id}/reject

Body:

{ "reject_reason": "抬头信息不完整", "suggestion": "请补充纳税人识别号" }

规则:仅允许对 pending 发票操作,写入驳回字段并更新 status=rejected。

6.3.5 管理员发票开票(支持覆盖重开)

POST /api/v1/invoices/{id}/issue

Body:

{ "invoice_file_url": "/files/invoices/INV202601210123456789_20260121103000.pdf", "issued_at": "2026-01-21T10:30:00Z" }

规则:

  • 允许从 pending/rejected 置为 issued
  • 允许在 issued 状态下覆盖更新 invoice_file_url/issued_at(“重开覆盖”)

6.3.6 管理端发票文件上传

POST /api/v1/invoices/upload

说明:

  • 存储目录:程序运行目录相对路径 ../files/invoices/
  • 文件名:{invoice_no}_{yyyyMMddHHmmss}.pdf
  • 返回相对 URL:/files/invoices/...pdf

FormData:

  • invoice_no
  • file(pdf)

6.4 公共下载接口(可选)

若需要邮件直达下载(不登录),建议采用 token 方式(不要直接暴露任意 file_url):

GET /api/public/invoices/download?token=xxxxx

规则:

  • token 对应 invoices.download_token(随机生成)
  • 仅当 status=issued 且存在 invoice_file_url 才允许下载

7. 错误码规划(建议)

建议新增发票模块错误码段:70xxxx

  • 700001 发票不存在
  • 700002 订单不允许开票(非 paid)
  • 700003 该订单已存在发票
  • 700004 发票状态不允许操作(如非 pending 不能驳回)
  • 700005 发票文件缺失/不可下载

8. 相关表结构(现有)

附录:现有表结构(参考)

-- license_manager.users definition

CREATE TABLE users ( id varchar(36) NOT NULL, username varchar(50) NOT NULL, email varchar(255) NOT NULL, password_hash varchar(255) NOT NULL, full_name varchar(100) NOT NULL, phone varchar(20) DEFAULT NULL, role varchar(20) NOT NULL DEFAULT 'viewer', status varchar(20) NOT NULL DEFAULT 'active', last_login_at datetime(3) DEFAULT NULL, last_login_ip varchar(45) DEFAULT NULL, login_attempts bigint NOT NULL DEFAULT '0', locked_until datetime(3) DEFAULT NULL, created_at datetime(3) NOT NULL, updated_at datetime(3) NOT NULL, PRIMARY KEY (id), UNIQUE KEY idx_users_username (username), UNIQUE KEY idx_users_email (email), KEY idx_users_role (role), KEY idx_users_status (status), KEY idx_users_last_login_at (last_login_at), KEY idx_users_locked_until (locked_until), KEY idx_users_created_at (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- license_manager.cu_users definition

CREATE TABLE cu_users ( id varchar(36) NOT NULL, customer_id varchar(36) NOT NULL, phone varchar(20) NOT NULL, phone_country_code varchar(10) NOT NULL DEFAULT '+86', password varchar(255) DEFAULT NULL, salt varchar(32) DEFAULT NULL, user_role varchar(20) NOT NULL DEFAULT 'member', real_name varchar(100) DEFAULT NULL, email varchar(255) DEFAULT NULL, status varchar(20) NOT NULL DEFAULT 'active', phone_verified tinyint(1) NOT NULL DEFAULT '1', email_verified tinyint(1) NOT NULL DEFAULT '0', last_login_at datetime(3) DEFAULT NULL, last_login_ip varchar(50) DEFAULT NULL, login_attempts bigint NOT NULL DEFAULT '0', locked_until datetime(3) DEFAULT NULL, avatar_url varchar(500) DEFAULT NULL, language varchar(10) NOT NULL DEFAULT 'zh-CN', timezone varchar(50) NOT NULL DEFAULT 'Asia/Shanghai', additional_info json DEFAULT NULL, remark text, created_at datetime(3) NOT NULL, updated_at datetime(3) NOT NULL, deleted_at datetime(3) DEFAULT NULL, PRIMARY KEY (id), KEY idx_cu_users_customer_id (customer_id), KEY idx_cu_users_email (email), KEY idx_cu_users_status (status), KEY idx_cu_users_last_login_at (last_login_at), KEY idx_cu_users_locked_until (locked_until), KEY idx_cu_users_created_at (created_at), KEY idx_cu_users_deleted_at (deleted_at), KEY idx_cu_users_phone (phone,phone_country_code), KEY idx_cu_users_customer_status (customer_id,status), KEY idx_cu_users_status_locked (status,locked_until) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- license_manager.cu_orders definition

CREATE TABLE cu_orders ( id varchar(36) NOT NULL, order_no varchar(50) NOT NULL, customer_id varchar(36) NOT NULL, cu_user_id varchar(36) NOT NULL, package_id varchar(50) NOT NULL, package_name varchar(100) NOT NULL, license_count bigint NOT NULL, unit_price decimal(10,2) NOT NULL, discount_rate decimal(3,2) NOT NULL DEFAULT '1.00', total_amount decimal(10,2) NOT NULL, status varchar(20) NOT NULL DEFAULT 'paid', authorization_code varchar(500) DEFAULT NULL, expired_at datetime(3) DEFAULT NULL, created_at datetime(3) NOT NULL, updated_at datetime(3) NOT NULL, deleted_at datetime(3) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY idx_cu_orders_order_no (order_no), KEY idx_cu_orders_customer_id (customer_id), KEY idx_cu_orders_cu_user_id (cu_user_id), KEY idx_cu_orders_package_id (package_id), KEY idx_cu_orders_status (status), KEY idx_cu_orders_expired_at (expired_at), KEY idx_cu_orders_created_at (created_at), KEY idx_cu_orders_deleted_at (deleted_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- license_manager.customers definition

CREATE TABLE customers ( id varchar(36) NOT NULL DEFAULT (uuid()), customer_code varchar(20) NOT NULL, customer_name varchar(200) NOT NULL, customer_type varchar(20) NOT NULL DEFAULT 'enterprise', contact_person varchar(100) NOT NULL, contact_title varchar(100) DEFAULT NULL, email varchar(255) DEFAULT NULL, phone varchar(20) DEFAULT NULL, address text, company_size varchar(20) DEFAULT NULL, customer_level varchar(20) NOT NULL DEFAULT 'basic', status varchar(20) NOT NULL DEFAULT 'active', description text, created_at datetime(3) NOT NULL, updated_at datetime(3) NOT NULL, created_by varchar(36) NOT NULL, updated_by varchar(36) DEFAULT NULL, deleted_at datetime(3) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY idx_customers_customer_code (customer_code), KEY idx_customers_customer_name (customer_name), KEY idx_customers_customer_type (customer_type), KEY idx_customers_customer_level (customer_level), KEY idx_customers_status (status), KEY idx_customers_created_at (created_at), KEY idx_customers_created_by (created_by), KEY idx_customers_deleted_at (deleted_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- license_manager.authorization_codes definition

CREATE TABLE authorization_codes ( id varchar(36) NOT NULL, code varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, customer_id varchar(36) NOT NULL, created_by varchar(36) NOT NULL, software_id varchar(50) DEFAULT NULL, description text, start_date datetime(3) NOT NULL, end_date datetime(3) NOT NULL, deployment_type varchar(20) NOT NULL DEFAULT 'standalone', encryption_type varchar(20) DEFAULT 'standard', software_version varchar(50) DEFAULT NULL, max_activations bigint NOT NULL DEFAULT '1', is_locked tinyint(1) NOT NULL DEFAULT '0', lock_reason text, locked_at datetime(3) DEFAULT NULL, locked_by varchar(36) DEFAULT NULL, feature_config json DEFAULT NULL, usage_limits json DEFAULT NULL, custom_parameters json DEFAULT NULL, created_at datetime(3) NOT NULL, updated_at datetime(3) NOT NULL, PRIMARY KEY (id), KEY idx_authorization_codes_customer_id (customer_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;