From b65b4ac801c17ec33be06a8ae1e08f6f76de68be Mon Sep 17 00:00:00 2001 From: wood911010 Date: Tue, 24 Feb 2026 18:50:47 +0800 Subject: [PATCH] feat: Implement initial customer authorization code and invoice management modules with API documentation and configuration. --- .../demand/2026.2.24-用户端发票申请修改.md | 80 +++ backend/docs/install/dev_install.md | 4 +- backend/docs/swagger/docs.go | 620 ++++++++++-------- backend/docs/swagger/swagger.json | 549 +++++++++------- backend/docs/swagger/swagger.yaml | 370 +++++++---- .../api/handlers/cu_invoice_handler.go | 51 ++ backend/internal/api/routes/router.go | 1 + backend/internal/models/cu_invoice_models.go | 10 + .../repository/cu_invoice_repository.go | 7 + .../internal/service/cu_invoice_service.go | 49 ++ 10 files changed, 1099 insertions(+), 642 deletions(-) create mode 100644 backend/docs/demand/2026.2.24-用户端发票申请修改.md diff --git a/backend/docs/demand/2026.2.24-用户端发票申请修改.md b/backend/docs/demand/2026.2.24-用户端发票申请修改.md new file mode 100644 index 0000000..c1dc8e1 --- /dev/null +++ b/backend/docs/demand/2026.2.24-用户端发票申请修改.md @@ -0,0 +1,80 @@ +# 2026.2.24-用户端发票申请修改 + +## 1. 需求背景 +目前用户申请发票后,如果被管理员驳回(状态为 `rejected`),用户无法修改该申请信息并重新提交。由于每个订单只能关联一个发票记录,用户也无法为该订单重新发起新的申请。因此需要增加一个发票修改接口,允许用户在驳回状态下编辑发票信息并重新进入待处理状态。 + +## 2. 业务流程 +1. **用户侧**:查看发票详情,发现状态为“已驳回”,查看“驳回原因”和“修改建议”。 +2. **用户侧**:点击修改,编辑发票抬头、税号、邮箱等信息。 +3. **提交修改**:调用修改接口。 + - 系统校验发票当前状态必须为 `rejected`。 + - 系统校验发票所属权。 + - 系统更新发票信息,并将状态重置为 `pending`。 + - 系统清空原有的驳回信息(可选,或保留历史但在界面不再作为当前驳回提示)。 +4. **管理侧**:发票重新出现在待处理列表中。 + +## 3. 接口设计 + +### 3.1 修改发票申请 (客户端) + +- **接口地址**:`PUT /api/cu/invoices/{id}` +- **鉴权**:需要客户端登录态 (`CustomerAuth`) +- **请求参数 (JSON Body)**: + +| 字段名 | 类型 | 必填 | 说明 | +| :--- | :--- | :--- | :--- | +| invoice_type | string | 是 | 发票类型: personal/enterprise/vat_special | +| receiver_email| string | 是 | 收票邮箱 | +| title | string | 是 | 发票抬头 | +| taxpayer_id | string | 否 | 纳税人识别号 (企业类型必填) | +| content | string | 否 | 开票内容 | +| remark | string | 否 | 备注 | + +> [!NOTE] +> `order_id` 不允许修改,因为发票记录与订单是一一绑定的。 + +- **逻辑校验**: + 1. **存在性校验**:发票 ID 必须存在。 + 2. **权限校验**:发票必须属于当前登录的客户用户 (`cu_user_id`)。 + 3. **状态校验**:只有 `status = 'rejected'` 的发票允许修改。若状态为 `pending` 或 `issued`,则返回错误。 + 4. **字段校验**:与创建发票时的校验逻辑一致(如企业类型必须有税号)。 + +- **数据更新**: + - 更新传入的业务字段。 + - `status` 更新为 `pending`。 + - `updated_at` 更新为当前时间。 + - `reject_reason`、`suggestion`、`rejected_at`、`rejected_by` 建议清空,以表示这是一次全新的提交。 + +### 3.2 响应示例 + +**成功响应**: +```json +{ + "code": "000000", + "message": "成功", + "data": { + "id": "invoice-uuid", + "invoice_no": "INV20260224001", + "status": "pending" + }, + "timestamp": "2026-02-24T18:00:00Z" +} +``` + +**失败响应 (状态错误)**: +```json +{ + "code": "700004", + "message": "发票当前状态不允许该操作", + "timestamp": "2026-02-24T18:00:00Z" +} +``` + +## 4. 影响范围 +- **后端模型**:需新增 `InvoiceUpdateRequest` 结构体。 +- **后端服务**:`CuInvoiceService` 新增 `UpdateInvoice` 方法。 +- **后端接口**:`router.go` 注册新路由。 +- **前端**:发票详情页增加修改按钮(仅在驳回时显示),跳转至编辑页。 + +## 5. 错误码补充 +- `700006`: 发票当前状态不允许修改 (如果 700004 语义不够具体可以新增,或沿用 700004)。 diff --git a/backend/docs/install/dev_install.md b/backend/docs/install/dev_install.md index e653c88..0c15fcf 100644 --- a/backend/docs/install/dev_install.md +++ b/backend/docs/install/dev_install.md @@ -2,8 +2,10 @@ ```bash # 使用默认配置 -go run cmd/main.go +cd cmd +go run . # 指定配置文件 +cd cmd go run . -config configs/config.dev.yaml ``` \ No newline at end of file diff --git a/backend/docs/swagger/docs.go b/backend/docs/swagger/docs.go index f4ba474..2005d43 100644 --- a/backend/docs/swagger/docs.go +++ b/backend/docs/swagger/docs.go @@ -1,17 +1,10 @@ -// Package swagger GENERATED BY THE COMMAND ABOVE; DO NOT EDIT -// This file was generated by swaggo/swag +// Code generated by swaggo/swag. DO NOT EDIT. + package swagger -import ( - "bytes" - "encoding/json" - "strings" - "text/template" +import "github.com/swaggo/swag" - "github.com/swaggo/swag" -) - -var doc = `{ +const docTemplate = `{ "schemes": {{ marshal .Schemes }}, "swagger": "2.0", "info": { @@ -737,25 +730,18 @@ var doc = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "id": { + "type": "string" }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "invoice_no": { - "type": "string" - }, - "status": { - "type": "string" - } - } + "invoice_no": { + "type": "string" + }, + "status": { + "type": "string" } - ] + } } } } @@ -912,6 +898,103 @@ var doc = `{ } } } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "客户修改已被驳回的发票申请并重新提交", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "发票管理" + ], + "summary": "客户修改发票申请", + "parameters": [ + { + "type": "string", + "description": "发票ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "发票修改请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.InvoiceUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "invoice_no": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "请求参数无效", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "发票不存在", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } } }, "/api/cu/invoices/{id}/download": { @@ -1011,52 +1094,38 @@ var doc = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "token": { + "type": "string" }, - { + "user": { "type": "object", "properties": { - "token": { + "customer_id": { "type": "string" }, - "user": { - "allOf": [ - { - "type": "object" - }, - { - "type": "object", - "properties": { - "customer_id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "id": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "real_name": { - "type": "string" - }, - "status": { - "type": "string" - }, - "user_role": { - "type": "string" - } - } - } - ] + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "real_name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "user_role": { + "type": "string" } } } - ] + } } } } @@ -1220,34 +1289,27 @@ var doc = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "expire_time": { + "type": "string" }, - { - "type": "object", - "properties": { - "expire_time": { - "type": "string" - }, - "id": { - "type": "string" - }, - "order_no": { - "type": "string" - }, - "payment_no": { - "type": "string" - }, - "payment_url": { - "type": "string" - }, - "total_amount": { - "type": "number" - } - } + "id": { + "type": "string" + }, + "order_no": { + "type": "string" + }, + "payment_no": { + "type": "string" + }, + "payment_url": { + "type": "string" + }, + "total_amount": { + "type": "number" } - ] + } } } } @@ -1306,22 +1368,15 @@ var doc = `{ "in": "body", "required": true, "schema": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "license_count": { + "type": "integer" }, - { - "type": "object", - "properties": { - "license_count": { - "type": "integer" - }, - "package_id": { - "type": "string" - } - } + "package_id": { + "type": "string" } - ] + } } } ], @@ -1337,31 +1392,24 @@ var doc = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "discount_description": { + "type": "string" }, - { - "type": "object", - "properties": { - "discount_description": { - "type": "string" - }, - "discount_rate": { - "type": "number" - }, - "license_count": { - "type": "integer" - }, - "total_amount": { - "type": "number" - }, - "unit_price": { - "type": "number" - } - } + "discount_rate": { + "type": "number" + }, + "license_count": { + "type": "integer" + }, + "total_amount": { + "type": "number" + }, + "unit_price": { + "type": "number" } - ] + } } } } @@ -1543,19 +1591,19 @@ var doc = `{ } }, "400": { - "description": "" + "description": "Bad Request" }, "401": { - "description": "" + "description": "Unauthorized" }, "403": { - "description": "" + "description": "Forbidden" }, "404": { - "description": "" + "description": "Not Found" }, "409": { - "description": "" + "description": "Conflict" } } } @@ -1609,34 +1657,27 @@ var doc = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "expire_time": { + "type": "string" }, - { - "type": "object", - "properties": { - "expire_time": { - "type": "string" - }, - "order_id": { - "type": "string" - }, - "order_no": { - "type": "string" - }, - "payment_no": { - "type": "string" - }, - "payment_url": { - "type": "string" - }, - "total_amount": { - "type": "number" - } - } + "order_id": { + "type": "string" + }, + "order_no": { + "type": "string" + }, + "payment_no": { + "type": "string" + }, + "payment_url": { + "type": "string" + }, + "total_amount": { + "type": "number" } - ] + } } } } @@ -1697,31 +1738,24 @@ var doc = `{ "data": { "type": "array", "items": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "id": { + "type": "string" }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "max_devices": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "type": { - "type": "string" - } - } + "max_devices": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "price": { + "type": "number" + }, + "type": { + "type": "string" } - ] + } } } } @@ -2128,52 +2162,38 @@ var doc = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "token": { + "type": "string" }, - { + "user": { "type": "object", "properties": { - "token": { + "customer_id": { "type": "string" }, - "user": { - "allOf": [ - { - "type": "object" - }, - { - "type": "object", - "properties": { - "customer_id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "id": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "real_name": { - "type": "string" - }, - "status": { - "type": "string" - }, - "user_role": { - "type": "string" - } - } - } - ] + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "real_name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "user_role": { + "type": "string" } } } - ] + } } } } @@ -6506,7 +6526,11 @@ var doc = `{ }, "customer_info": { "description": "客户信息(仅在详情接口返回)", - "$ref": "#/definitions/models.CustomerInfoForAuthCode" + "allOf": [ + { + "$ref": "#/definitions/models.CustomerInfoForAuthCode" + } + ] }, "customer_name": { "description": "客户名称", @@ -6815,7 +6839,11 @@ var doc = `{ "properties": { "new_authorization_code": { "description": "新生成的授权码信息", - "$ref": "#/definitions/models.AuthorizationCodeShareResponseItem" + "allOf": [ + { + "$ref": "#/definitions/models.AuthorizationCodeShareResponseItem" + } + ] } } }, @@ -7133,6 +7161,10 @@ var doc = `{ "description": "订单过期时间", "type": "string" }, + "has_invoice_applied": { + "description": "是否已申请发票(根据 invoices 表判断)", + "type": "boolean" + }, "id": { "description": "订单ID", "type": "string" @@ -7528,7 +7560,11 @@ var doc = `{ }, "authorization_stats": { "description": "授权统计信息(仅在详情接口返回)", - "$ref": "#/definitions/models.AuthorizationStats" + "allOf": [ + { + "$ref": "#/definitions/models.AuthorizationStats" + } + ] }, "company_size": { "type": "string" @@ -7853,11 +7889,19 @@ var doc = `{ "properties": { "period": { "description": "时间段信息", - "$ref": "#/definitions/models.TrendPeriod" + "allOf": [ + { + "$ref": "#/definitions/models.TrendPeriod" + } + ] }, "summary": { "description": "汇总信息", - "$ref": "#/definitions/models.TrendSummary" + "allOf": [ + { + "$ref": "#/definitions/models.TrendSummary" + } + ] }, "trend_data": { "description": "趋势数据", @@ -7893,7 +7937,11 @@ var doc = `{ }, "authorization_info": { "description": "授权信息", - "$ref": "#/definitions/models.AuthorizationInfo" + "allOf": [ + { + "$ref": "#/definitions/models.AuthorizationInfo" + } + ] }, "device_info": { "description": "设备信息", @@ -8489,6 +8537,47 @@ var doc = `{ } } }, + "models.InvoiceUpdateRequest": { + "type": "object", + "required": [ + "invoice_type", + "receiver_email", + "title" + ], + "properties": { + "content": { + "description": "开票内容,非必填,最多200字符", + "type": "string", + "maxLength": 200 + }, + "invoice_type": { + "description": "发票类型,必填,personal-个人/enterprise-企业普票/vat_special-增值税专用发票", + "type": "string", + "enum": [ + "personal", + "enterprise", + "vat_special" + ] + }, + "receiver_email": { + "description": "收票邮箱,必填,格式为邮箱地址", + "type": "string" + }, + "remark": { + "description": "备注,可选", + "type": "string" + }, + "taxpayer_id": { + "description": "纳税人识别号,企业发票必填,个人发票可为空", + "type": "string" + }, + "title": { + "description": "发票抬头,必填,最多200字符", + "type": "string", + "maxLength": 200 + } + } + }, "models.InvoiceUploadResponse": { "type": "object", "properties": { @@ -8717,7 +8806,11 @@ var doc = `{ }, "authorization_code": { "description": "关联字段(用于查询时的JOIN)", - "$ref": "#/definitions/models.AuthorizationCode" + "allOf": [ + { + "$ref": "#/definitions/models.AuthorizationCode" + } + ] }, "authorization_code_id": { "type": "string" @@ -9303,7 +9396,11 @@ var doc = `{ }, "business_order": { "description": "关联的业务订单信息", - "$ref": "#/definitions/models.PaymentBusinessOrder" + "allOf": [ + { + "$ref": "#/definitions/models.PaymentBusinessOrder" + } + ] }, "payment_no": { "description": "支付单号", @@ -9418,7 +9515,11 @@ var doc = `{ }, "growth_rate": { "description": "增长率(同比上月)", - "$ref": "#/definitions/models.GrowthRate" + "allOf": [ + { + "$ref": "#/definitions/models.GrowthRate" + } + ] }, "total_auth_codes": { "description": "总授权码数量(所有授权码的累计总数)", @@ -9512,6 +9613,7 @@ var doc = `{ }, "securityDefinitions": { "BearerAuth": { + "description": "Bearer token for authentication", "type": "apiKey", "name": "Authorization", "in": "header" @@ -9519,56 +9621,18 @@ var doc = `{ } }` -type swaggerInfo struct { - Version string - Host string - BasePath string - Schemes []string - Title string - Description string -} - // SwaggerInfo holds exported Swagger Info so clients can modify it -var SwaggerInfo = swaggerInfo{ - Version: "1.0", - Host: "", - BasePath: "/", - Schemes: []string{"http", "https"}, - Title: "License Manager API", - Description: "Bearer token for authentication", -} - -type s struct{} - -func (s *s) ReadDoc() string { - sInfo := SwaggerInfo - sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) - - t, err := template.New("swagger_info").Funcs(template.FuncMap{ - "marshal": func(v interface{}) string { - a, _ := json.Marshal(v) - return string(a) - }, - "escape": func(v interface{}) string { - // escape tabs - str := strings.Replace(v.(string), "\t", "\\t", -1) - // replace " with \", and if that results in \\", replace that with \\\" - str = strings.Replace(str, "\"", "\\\"", -1) - return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1) - }, - }).Parse(doc) - if err != nil { - return doc - } - - var tpl bytes.Buffer - if err := t.Execute(&tpl, sInfo); err != nil { - return doc - } - - return tpl.String() +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "", + BasePath: "/", + Schemes: []string{"http", "https"}, + Title: "License Manager API", + Description: "软件授权管理平台API文档", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, } func init() { - swag.Register("swagger", &s{}) + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) } diff --git a/backend/docs/swagger/swagger.json b/backend/docs/swagger/swagger.json index 75e7391..f819532 100644 --- a/backend/docs/swagger/swagger.json +++ b/backend/docs/swagger/swagger.json @@ -5,7 +5,7 @@ ], "swagger": "2.0", "info": { - "description": "Bearer token for authentication", + "description": "软件授权管理平台API文档", "title": "License Manager API", "termsOfService": "http://swagger.io/terms/", "contact": { @@ -726,25 +726,18 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "id": { + "type": "string" }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "invoice_no": { - "type": "string" - }, - "status": { - "type": "string" - } - } + "invoice_no": { + "type": "string" + }, + "status": { + "type": "string" } - ] + } } } } @@ -901,6 +894,103 @@ } } } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "客户修改已被驳回的发票申请并重新提交", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "发票管理" + ], + "summary": "客户修改发票申请", + "parameters": [ + { + "type": "string", + "description": "发票ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "发票修改请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.InvoiceUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "invoice_no": { + "type": "string" + }, + "status": { + "type": "string" + } + } + } + } + } + ] + } + }, + "400": { + "description": "请求参数无效", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "403": { + "description": "权限不足", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "404": { + "description": "发票不存在", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + }, + "500": { + "description": "服务器内部错误", + "schema": { + "$ref": "#/definitions/models.ErrorResponse" + } + } + } } }, "/api/cu/invoices/{id}/download": { @@ -1000,52 +1090,38 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "token": { + "type": "string" }, - { + "user": { "type": "object", "properties": { - "token": { + "customer_id": { "type": "string" }, - "user": { - "allOf": [ - { - "type": "object" - }, - { - "type": "object", - "properties": { - "customer_id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "id": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "real_name": { - "type": "string" - }, - "status": { - "type": "string" - }, - "user_role": { - "type": "string" - } - } - } - ] + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "real_name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "user_role": { + "type": "string" } } } - ] + } } } } @@ -1209,34 +1285,27 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "expire_time": { + "type": "string" }, - { - "type": "object", - "properties": { - "expire_time": { - "type": "string" - }, - "id": { - "type": "string" - }, - "order_no": { - "type": "string" - }, - "payment_no": { - "type": "string" - }, - "payment_url": { - "type": "string" - }, - "total_amount": { - "type": "number" - } - } + "id": { + "type": "string" + }, + "order_no": { + "type": "string" + }, + "payment_no": { + "type": "string" + }, + "payment_url": { + "type": "string" + }, + "total_amount": { + "type": "number" } - ] + } } } } @@ -1295,22 +1364,15 @@ "in": "body", "required": true, "schema": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "license_count": { + "type": "integer" }, - { - "type": "object", - "properties": { - "license_count": { - "type": "integer" - }, - "package_id": { - "type": "string" - } - } + "package_id": { + "type": "string" } - ] + } } } ], @@ -1326,31 +1388,24 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "discount_description": { + "type": "string" }, - { - "type": "object", - "properties": { - "discount_description": { - "type": "string" - }, - "discount_rate": { - "type": "number" - }, - "license_count": { - "type": "integer" - }, - "total_amount": { - "type": "number" - }, - "unit_price": { - "type": "number" - } - } + "discount_rate": { + "type": "number" + }, + "license_count": { + "type": "integer" + }, + "total_amount": { + "type": "number" + }, + "unit_price": { + "type": "number" } - ] + } } } } @@ -1532,19 +1587,19 @@ } }, "400": { - "description": "" + "description": "Bad Request" }, "401": { - "description": "" + "description": "Unauthorized" }, "403": { - "description": "" + "description": "Forbidden" }, "404": { - "description": "" + "description": "Not Found" }, "409": { - "description": "" + "description": "Conflict" } } } @@ -1598,34 +1653,27 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "expire_time": { + "type": "string" }, - { - "type": "object", - "properties": { - "expire_time": { - "type": "string" - }, - "order_id": { - "type": "string" - }, - "order_no": { - "type": "string" - }, - "payment_no": { - "type": "string" - }, - "payment_url": { - "type": "string" - }, - "total_amount": { - "type": "number" - } - } + "order_id": { + "type": "string" + }, + "order_no": { + "type": "string" + }, + "payment_no": { + "type": "string" + }, + "payment_url": { + "type": "string" + }, + "total_amount": { + "type": "number" } - ] + } } } } @@ -1686,31 +1734,24 @@ "data": { "type": "array", "items": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "id": { + "type": "string" }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "max_devices": { - "type": "integer" - }, - "name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "type": { - "type": "string" - } - } + "max_devices": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "price": { + "type": "number" + }, + "type": { + "type": "string" } - ] + } } } } @@ -2117,52 +2158,38 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "type": "object" + "type": "object", + "properties": { + "token": { + "type": "string" }, - { + "user": { "type": "object", "properties": { - "token": { + "customer_id": { "type": "string" }, - "user": { - "allOf": [ - { - "type": "object" - }, - { - "type": "object", - "properties": { - "customer_id": { - "type": "string" - }, - "email": { - "type": "string" - }, - "id": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "real_name": { - "type": "string" - }, - "status": { - "type": "string" - }, - "user_role": { - "type": "string" - } - } - } - ] + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "real_name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "user_role": { + "type": "string" } } } - ] + } } } } @@ -6495,7 +6522,11 @@ }, "customer_info": { "description": "客户信息(仅在详情接口返回)", - "$ref": "#/definitions/models.CustomerInfoForAuthCode" + "allOf": [ + { + "$ref": "#/definitions/models.CustomerInfoForAuthCode" + } + ] }, "customer_name": { "description": "客户名称", @@ -6804,7 +6835,11 @@ "properties": { "new_authorization_code": { "description": "新生成的授权码信息", - "$ref": "#/definitions/models.AuthorizationCodeShareResponseItem" + "allOf": [ + { + "$ref": "#/definitions/models.AuthorizationCodeShareResponseItem" + } + ] } } }, @@ -7122,6 +7157,10 @@ "description": "订单过期时间", "type": "string" }, + "has_invoice_applied": { + "description": "是否已申请发票(根据 invoices 表判断)", + "type": "boolean" + }, "id": { "description": "订单ID", "type": "string" @@ -7517,7 +7556,11 @@ }, "authorization_stats": { "description": "授权统计信息(仅在详情接口返回)", - "$ref": "#/definitions/models.AuthorizationStats" + "allOf": [ + { + "$ref": "#/definitions/models.AuthorizationStats" + } + ] }, "company_size": { "type": "string" @@ -7842,11 +7885,19 @@ "properties": { "period": { "description": "时间段信息", - "$ref": "#/definitions/models.TrendPeriod" + "allOf": [ + { + "$ref": "#/definitions/models.TrendPeriod" + } + ] }, "summary": { "description": "汇总信息", - "$ref": "#/definitions/models.TrendSummary" + "allOf": [ + { + "$ref": "#/definitions/models.TrendSummary" + } + ] }, "trend_data": { "description": "趋势数据", @@ -7882,7 +7933,11 @@ }, "authorization_info": { "description": "授权信息", - "$ref": "#/definitions/models.AuthorizationInfo" + "allOf": [ + { + "$ref": "#/definitions/models.AuthorizationInfo" + } + ] }, "device_info": { "description": "设备信息", @@ -8478,6 +8533,47 @@ } } }, + "models.InvoiceUpdateRequest": { + "type": "object", + "required": [ + "invoice_type", + "receiver_email", + "title" + ], + "properties": { + "content": { + "description": "开票内容,非必填,最多200字符", + "type": "string", + "maxLength": 200 + }, + "invoice_type": { + "description": "发票类型,必填,personal-个人/enterprise-企业普票/vat_special-增值税专用发票", + "type": "string", + "enum": [ + "personal", + "enterprise", + "vat_special" + ] + }, + "receiver_email": { + "description": "收票邮箱,必填,格式为邮箱地址", + "type": "string" + }, + "remark": { + "description": "备注,可选", + "type": "string" + }, + "taxpayer_id": { + "description": "纳税人识别号,企业发票必填,个人发票可为空", + "type": "string" + }, + "title": { + "description": "发票抬头,必填,最多200字符", + "type": "string", + "maxLength": 200 + } + } + }, "models.InvoiceUploadResponse": { "type": "object", "properties": { @@ -8706,7 +8802,11 @@ }, "authorization_code": { "description": "关联字段(用于查询时的JOIN)", - "$ref": "#/definitions/models.AuthorizationCode" + "allOf": [ + { + "$ref": "#/definitions/models.AuthorizationCode" + } + ] }, "authorization_code_id": { "type": "string" @@ -9292,7 +9392,11 @@ }, "business_order": { "description": "关联的业务订单信息", - "$ref": "#/definitions/models.PaymentBusinessOrder" + "allOf": [ + { + "$ref": "#/definitions/models.PaymentBusinessOrder" + } + ] }, "payment_no": { "description": "支付单号", @@ -9407,7 +9511,11 @@ }, "growth_rate": { "description": "增长率(同比上月)", - "$ref": "#/definitions/models.GrowthRate" + "allOf": [ + { + "$ref": "#/definitions/models.GrowthRate" + } + ] }, "total_auth_codes": { "description": "总授权码数量(所有授权码的累计总数)", @@ -9501,6 +9609,7 @@ }, "securityDefinitions": { "BearerAuth": { + "description": "Bearer token for authentication", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/backend/docs/swagger/swagger.yaml b/backend/docs/swagger/swagger.yaml index d7f1a17..93ad6ad 100644 --- a/backend/docs/swagger/swagger.yaml +++ b/backend/docs/swagger/swagger.yaml @@ -109,7 +109,8 @@ definitions: description: 客户ID type: string customer_info: - $ref: '#/definitions/models.CustomerInfoForAuthCode' + allOf: + - $ref: '#/definitions/models.CustomerInfoForAuthCode' description: 客户信息(仅在详情接口返回) customer_name: description: 客户名称 @@ -337,7 +338,8 @@ definitions: models.AuthorizationCodeShareResponse: properties: new_authorization_code: - $ref: '#/definitions/models.AuthorizationCodeShareResponseItem' + allOf: + - $ref: '#/definitions/models.AuthorizationCodeShareResponseItem' description: 新生成的授权码信息 type: object models.AuthorizationCodeShareResponseItem: @@ -567,6 +569,9 @@ definitions: expired_at: description: 订单过期时间 type: string + has_invoice_applied: + description: 是否已申请发票(根据 invoices 表判断) + type: boolean id: description: 订单ID type: string @@ -860,7 +865,8 @@ definitions: address: type: string authorization_stats: - $ref: '#/definitions/models.AuthorizationStats' + allOf: + - $ref: '#/definitions/models.AuthorizationStats' description: 授权统计信息(仅在详情接口返回) company_size: type: string @@ -1094,10 +1100,12 @@ definitions: models.DashboardAuthorizationTrendResponse: properties: period: - $ref: '#/definitions/models.TrendPeriod' + allOf: + - $ref: '#/definitions/models.TrendPeriod' description: 时间段信息 summary: - $ref: '#/definitions/models.TrendSummary' + allOf: + - $ref: '#/definitions/models.TrendSummary' description: 汇总信息 trend_data: description: 趋势数据 @@ -1122,7 +1130,8 @@ definitions: description: 激活时间 type: string authorization_info: - $ref: '#/definitions/models.AuthorizationInfo' + allOf: + - $ref: '#/definitions/models.AuthorizationInfo' description: 授权信息 device_info: additionalProperties: true @@ -1556,6 +1565,37 @@ definitions: description: 发票总数 type: integer type: object + models.InvoiceUpdateRequest: + properties: + content: + description: 开票内容,非必填,最多200字符 + maxLength: 200 + type: string + invoice_type: + description: 发票类型,必填,personal-个人/enterprise-企业普票/vat_special-增值税专用发票 + enum: + - personal + - enterprise + - vat_special + type: string + receiver_email: + description: 收票邮箱,必填,格式为邮箱地址 + type: string + remark: + description: 备注,可选 + type: string + taxpayer_id: + description: 纳税人识别号,企业发票必填,个人发票可为空 + type: string + title: + description: 发票抬头,必填,最多200字符 + maxLength: 200 + type: string + required: + - invoice_type + - receiver_email + - title + type: object models.InvoiceUploadResponse: properties: file_url: @@ -1725,7 +1765,8 @@ definitions: activation_ip: type: string authorization_code: - $ref: '#/definitions/models.AuthorizationCode' + allOf: + - $ref: '#/definitions/models.AuthorizationCode' description: 关联字段(用于查询时的JOIN) authorization_code_id: type: string @@ -2139,7 +2180,8 @@ definitions: description: 支付金额,单位元 type: number business_order: - $ref: '#/definitions/models.PaymentBusinessOrder' + allOf: + - $ref: '#/definitions/models.PaymentBusinessOrder' description: 关联的业务订单信息 payment_no: description: 支付单号 @@ -2222,7 +2264,8 @@ definitions: description: 即将过期数量(30天内即将过期的授权码,且未被锁定) type: integer growth_rate: - $ref: '#/definitions/models.GrowthRate' + allOf: + - $ref: '#/definitions/models.GrowthRate' description: 增长率(同比上月) total_auth_codes: description: 总授权码数量(所有授权码的累计总数) @@ -2291,7 +2334,7 @@ info: email: support@swagger.io name: API Support url: http://www.swagger.io/support - description: Bearer token for authentication + description: 软件授权管理平台API文档 license: name: MIT url: https://opensource.org/licenses/MIT @@ -2730,16 +2773,14 @@ paths: - $ref: '#/definitions/models.APIResponse' - properties: data: - allOf: - - type: object - - properties: - id: - type: string - invoice_no: - type: string - status: - type: string - type: object + properties: + id: + type: string + invoice_no: + type: string + status: + type: string + type: object type: object "400": description: 请求参数无效 @@ -2806,6 +2847,66 @@ paths: summary: 客户发票详情 tags: - 发票管理 + put: + consumes: + - application/json + description: 客户修改已被驳回的发票申请并重新提交 + parameters: + - description: 发票ID + in: path + name: id + required: true + type: string + - description: 发票修改请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.InvoiceUpdateRequest' + produces: + - application/json + responses: + "200": + description: 成功 + schema: + allOf: + - $ref: '#/definitions/models.APIResponse' + - properties: + data: + properties: + id: + type: string + invoice_no: + type: string + status: + type: string + type: object + type: object + "400": + description: 请求参数无效 + schema: + $ref: '#/definitions/models.ErrorResponse' + "401": + description: 未认证 + schema: + $ref: '#/definitions/models.ErrorResponse' + "403": + description: 权限不足 + schema: + $ref: '#/definitions/models.ErrorResponse' + "404": + description: 发票不存在 + schema: + $ref: '#/definitions/models.ErrorResponse' + "500": + description: 服务器内部错误 + schema: + $ref: '#/definitions/models.ErrorResponse' + security: + - BearerAuth: [] + summary: 客户修改发票申请 + tags: + - 发票管理 /api/cu/invoices/{id}/download: get: consumes: @@ -2897,31 +2998,27 @@ paths: - $ref: '#/definitions/models.APIResponse' - properties: data: - allOf: - - type: object - - properties: - token: - type: string - user: - allOf: - - type: object - - properties: - customer_id: - type: string - email: - type: string - id: - type: string - phone: - type: string - real_name: - type: string - status: - type: string - user_role: - type: string - type: object - type: object + properties: + token: + type: string + user: + properties: + customer_id: + type: string + email: + type: string + id: + type: string + phone: + type: string + real_name: + type: string + status: + type: string + user_role: + type: string + type: object + type: object type: object "400": description: 请求参数无效 @@ -3021,22 +3118,20 @@ paths: - $ref: '#/definitions/models.APIResponse' - properties: data: - allOf: - - type: object - - properties: - expire_time: - type: string - id: - type: string - order_no: - type: string - payment_no: - type: string - payment_url: - type: string - total_amount: - type: number - type: object + properties: + expire_time: + type: string + id: + type: string + order_no: + type: string + payment_no: + type: string + payment_url: + type: string + total_amount: + type: number + type: object type: object "400": description: 请求参数无效 @@ -3118,15 +3213,15 @@ paths: schema: $ref: '#/definitions/models.APIResponse' "400": - description: "" + description: Bad Request "401": - description: "" + description: Unauthorized "403": - description: "" + description: Forbidden "404": - description: "" + description: Not Found "409": - description: "" + description: Conflict security: - BearerAuth: [] summary: 删除订单 @@ -3160,22 +3255,20 @@ paths: - $ref: '#/definitions/models.APIResponse' - properties: data: - allOf: - - type: object - - properties: - expire_time: - type: string - order_id: - type: string - order_no: - type: string - payment_no: - type: string - payment_url: - type: string - total_amount: - type: number - type: object + properties: + expire_time: + type: string + order_id: + type: string + order_no: + type: string + payment_no: + type: string + payment_url: + type: string + total_amount: + type: number + type: object type: object "400": description: 订单状态不允许继续支付 @@ -3209,14 +3302,12 @@ paths: name: request required: true schema: - allOf: - - type: object - - properties: - license_count: - type: integer - package_id: - type: string - type: object + properties: + license_count: + type: integer + package_id: + type: string + type: object produces: - application/json responses: @@ -3227,20 +3318,18 @@ paths: - $ref: '#/definitions/models.APIResponse' - properties: data: - allOf: - - type: object - - properties: - discount_description: - type: string - discount_rate: - type: number - license_count: - type: integer - total_amount: - type: number - unit_price: - type: number - type: object + properties: + discount_description: + type: string + discount_rate: + type: number + license_count: + type: integer + total_amount: + type: number + unit_price: + type: number + type: object type: object "400": description: 请求参数无效 @@ -3305,20 +3394,18 @@ paths: - properties: data: items: - allOf: - - type: object - - properties: - id: - type: string - max_devices: - type: integer - name: - type: string - price: - type: number - type: - type: string - type: object + properties: + id: + type: string + max_devices: + type: integer + name: + type: string + price: + type: number + type: + type: string + type: object type: array type: object summary: 获取套餐列表 @@ -3572,31 +3659,27 @@ paths: - $ref: '#/definitions/models.APIResponse' - properties: data: - allOf: - - type: object - - properties: - token: - type: string - user: - allOf: - - type: object - - properties: - customer_id: - type: string - email: - type: string - id: - type: string - phone: - type: string - real_name: - type: string - status: - type: string - user_role: - type: string - type: object - type: object + properties: + token: + type: string + user: + properties: + customer_id: + type: string + email: + type: string + id: + type: string + phone: + type: string + real_name: + type: string + status: + type: string + user_role: + type: string + type: object + type: object type: object "400": description: 请求参数无效 @@ -6196,6 +6279,7 @@ schemes: - https securityDefinitions: BearerAuth: + description: Bearer token for authentication in: header name: Authorization type: apiKey diff --git a/backend/internal/api/handlers/cu_invoice_handler.go b/backend/internal/api/handlers/cu_invoice_handler.go index 8bdbe87..69fe6b4 100644 --- a/backend/internal/api/handlers/cu_invoice_handler.go +++ b/backend/internal/api/handlers/cu_invoice_handler.go @@ -200,3 +200,54 @@ func (h *CuInvoiceHandler) DownloadInvoice(c *gin.Context) { // 重定向到文件URL c.Redirect(http.StatusFound, fileURL) } + +// UpdateInvoice 客户修改发票申请 +// @Summary 客户修改发票申请 +// @Description 客户修改已被驳回的发票申请并重新提交 +// @Tags 发票管理 +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "发票ID" +// @Param request body models.InvoiceUpdateRequest true "发票修改请求" +// @Success 200 {object} models.APIResponse{data=object{id=string,invoice_no=string,status=string}} "成功" +// @Failure 400 {object} models.ErrorResponse "请求参数无效" +// @Failure 401 {object} models.ErrorResponse "未认证" +// @Failure 403 {object} models.ErrorResponse "权限不足" +// @Failure 404 {object} models.ErrorResponse "发票不存在" +// @Failure 500 {object} models.ErrorResponse "服务器内部错误" +// @Router /api/cu/invoices/{id} [put] +func (h *CuInvoiceHandler) UpdateInvoice(c *gin.Context) { + lang := middleware.GetLanguage(c) + invoiceID := c.Param("id") + + var req models.InvoiceUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse{ + Code: "900001", + Message: i18n.GetI18nErrorMessage("900001", lang), + Timestamp: getCurrentTimestamp(), + }) + return + } + + invoice, err := h.invoiceService.UpdateInvoice(c.Request.Context(), invoiceID, getCuUserID(c), &req) + if err != nil { + handleI18nError(c, err, lang) + return + } + + response := invoice.ToResponse() + result := map[string]interface{}{ + "id": response.ID, + "invoice_no": response.InvoiceNo, + "status": response.Status, + } + + c.JSON(http.StatusOK, models.APIResponse{ + Code: "000000", + Message: i18n.GetI18nErrorMessage("000000", lang), + Data: result, + Timestamp: getCurrentTimestamp(), + }) +} diff --git a/backend/internal/api/routes/router.go b/backend/internal/api/routes/router.go index 16f32fe..d3816b2 100644 --- a/backend/internal/api/routes/router.go +++ b/backend/internal/api/routes/router.go @@ -288,6 +288,7 @@ func SetupRouter() *gin.Engine { cuAuth.POST("/invoices", cuInvoiceHandler.CreateInvoice) cuAuth.GET("/invoices", cuInvoiceHandler.GetUserInvoices) cuAuth.GET("/invoices/:id", cuInvoiceHandler.GetUserInvoiceDetail) + cuAuth.PUT("/invoices/:id", cuInvoiceHandler.UpdateInvoice) cuAuth.GET("/invoices/summary", cuInvoiceHandler.GetUserInvoiceSummary) cuAuth.GET("/invoices/:id/download", cuInvoiceHandler.DownloadInvoice) } diff --git a/backend/internal/models/cu_invoice_models.go b/backend/internal/models/cu_invoice_models.go index 0ccefd6..0247b70 100644 --- a/backend/internal/models/cu_invoice_models.go +++ b/backend/internal/models/cu_invoice_models.go @@ -14,6 +14,16 @@ type InvoiceCreateRequest struct { Remark *string `json:"remark,omitempty"` // 备注,可选 } +// InvoiceUpdateRequest 客户端修改发票请求结构 +type InvoiceUpdateRequest struct { + InvoiceType string `json:"invoice_type" binding:"required,oneof=personal enterprise vat_special"` // 发票类型,必填,personal-个人/enterprise-企业普票/vat_special-增值税专用发票 + ReceiverEmail string `json:"receiver_email" binding:"required,email"` // 收票邮箱,必填,格式为邮箱地址 + Title string `json:"title" binding:"required,max=200"` // 发票抬头,必填,最多200字符 + TaxpayerID *string `json:"taxpayer_id,omitempty"` // 纳税人识别号,企业发票必填,个人发票可为空 + Content string `json:"content" binding:"omitempty,max=200"` // 开票内容,非必填,最多200字符 + Remark *string `json:"remark,omitempty"` // 备注,可选 +} + // InvoiceListRequest 发票列表查询请求结构 // 注:apply_date 支持 YYYY-MM-DD 或 RFC3339 时间范围 "start,end"。 type InvoiceListRequest struct { diff --git a/backend/internal/repository/cu_invoice_repository.go b/backend/internal/repository/cu_invoice_repository.go index 712af28..1dd0498 100644 --- a/backend/internal/repository/cu_invoice_repository.go +++ b/backend/internal/repository/cu_invoice_repository.go @@ -17,6 +17,7 @@ type CuInvoiceRepository interface { // ========== 通用工具方法 ========== Create(invoice *models.Invoice) error + Update(invoice *models.Invoice) error GetByID(id string) (*models.Invoice, error) CheckOrderInvoiceExists(orderID string) (bool, error) } @@ -33,6 +34,12 @@ func (r *cuInvoiceRepository) Create(invoice *models.Invoice) error { return r.db.Create(invoice).Error } +func (r *cuInvoiceRepository) Update(invoice *models.Invoice) error { + // 使用 Select 指定更新字段,确保可以清空某些字段(虽然此处目前主要是业务更新和状态重置) + // 或者直接使用 Save 更新全量字段 + return r.db.Save(invoice).Error +} + func (r *cuInvoiceRepository) GetByID(id string) (*models.Invoice, error) { var invoice models.Invoice err := r.db.Where("id = ? AND deleted_at IS NULL", id).First(&invoice).Error diff --git a/backend/internal/service/cu_invoice_service.go b/backend/internal/service/cu_invoice_service.go index 992871d..0d99929 100644 --- a/backend/internal/service/cu_invoice_service.go +++ b/backend/internal/service/cu_invoice_service.go @@ -21,6 +21,7 @@ type CuInvoiceService interface { GetUserInvoices(ctx context.Context, cuUserID string, req *models.InvoiceListRequest) (*models.InvoiceListResponse, error) GetInvoiceSummary(ctx context.Context, cuUserID string) (*models.InvoiceSummaryResponse, error) DownloadInvoice(ctx context.Context, invoiceID, cuUserID string) (string, error) + UpdateInvoice(ctx context.Context, invoiceID, cuUserID string, req *models.InvoiceUpdateRequest) (*models.Invoice, error) } type cuInvoiceService struct { @@ -175,6 +176,54 @@ func (s *cuInvoiceService) DownloadInvoice(ctx context.Context, invoiceID, cuUse return *invoice.InvoiceFileURL, nil } +func (s *cuInvoiceService) UpdateInvoice(ctx context.Context, invoiceID, cuUserID string, req *models.InvoiceUpdateRequest) (*models.Invoice, error) { + lang := pkgcontext.GetLanguageFromContext(ctx) + + // 获取原发票 + invoice, err := s.repo.GetByID(invoiceID) + if err != nil { + return nil, i18n.NewI18nError("700001", lang) + } + + // 权限校验 + if invoice.CuUserID != cuUserID { + return nil, i18n.NewI18nError("700001", lang) + } + + // 状态校验:只有被驳回的发票可以修改 + if invoice.Status != models.InvoiceStatusRejected { + return nil, i18n.NewI18nError("700004", lang) + } + + // 业务逻辑校验:发票类型和纳税人识别号 + if req.InvoiceType != models.InvoiceTypePersonal && (req.TaxpayerID == nil || *req.TaxpayerID == "") { + return nil, i18n.NewI18nError("700004", lang) + } + + // 更新字段 + invoice.InvoiceType = req.InvoiceType + invoice.Title = req.Title + invoice.TaxpayerID = req.TaxpayerID + invoice.Content = req.Content + invoice.ReceiverEmail = req.ReceiverEmail + invoice.Remark = req.Remark + + // 重置状态和驳回信息 + invoice.Status = models.InvoiceStatusPending + invoice.RejectReason = nil + invoice.Suggestion = nil + invoice.RejectedAt = nil + invoice.RejectedBy = nil + + // 更新数据库 + err = s.repo.Update(invoice) + if err != nil { + return nil, err + } + + return invoice, nil +} + // ========== 私有辅助方法 ========== // fillDisplayFields 填充响应结构的多语言显示字段