15 KiB
发票模块设计
1. 背景与目标
客户购买套餐后(订单已支付),需要在用户端发起开票申请;管理员在管理端处理开票、上传发票 PDF,或驳回并给出修改建议;用户可在列表/详情查看处理进度并下载发票文件。
2. 实体关系
cu_orders(订单) :invoices(发票) = 1 : 1(一个订单最多一张发票)
约束:
invoices.order_id唯一,确保 1:1
3. 状态与枚举
3.1 开票状态(invoice_status)
pending:待处理issued:已开票rejected:已驳回
状态流转:
pending -> issuedpending -> 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/rejectedsearch(可选):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_countpending_countissued_countrejected_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_nofile(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;