优化结构

This commit is contained in:
Yvan 2026-08-28 11:10:57 +08:00
parent 5b17f47141
commit 676fa4d952
10 changed files with 91 additions and 80 deletions

View File

@ -6,15 +6,22 @@ agents must follow when changing the template.
## Project structure ## Project structure
``` ```
api/<domain>/<version>/ Proto sources and generated stubs. Public contract. cmd/ Entrypoint, Wire injector, main.go.
cmd/<app>/ Entrypoint, Wire injector, main.go.
configs/ Runtime config (config.yaml). No secrets. configs/ Runtime config (config.yaml). No secrets.
internal/config/ Viper config models, loading, snapshots, and reloads. internal/config/ Viper config models, loading, snapshots, and reloads.
internal/global/ Process-wide shared runtime resources. internal/server/ Gin/Kratos handlers, middleware, routers, static files.
internal/server/ HTTP/gRPC server wiring. internal/service/dto/ Hand-written request and response DTOs.
internal/service/ Transport adapters; one file per resource. internal/service/ DTO/DO transport adapters.
internal/biz/ Domain models, usecases, repo interfaces, errors. internal/biz/ Domain models, usecases, repo interfaces, errors.
internal/data/ Repo implementations and storage clients. internal/data/ Repo implementations, database clients, migrations.
internal/initialize/ First-install and configuration orchestration.
internal/integration/ External I/O adapters (cache, email, payment, storage).
internal/modules/ Built-in module schema, seed, menu, API, and task contributions.
internal/routecatalog/ HTTP route metadata and runtime policies.
internal/worker/ Timed-task runtime and scheduler.
internal/utils/ Stateless internal helpers.
pkg/ Reusable infrastructure packages.
web/ Vue administration frontend.
``` ```
## Layering & dependency rules ## Layering & dependency rules
@ -28,7 +35,7 @@ owns the PO; `service` is a pass-through that converts at its boundary.
│ declares │ implements │ declares │ implements
└─── repo IF ────┘ └─── repo IF ────┘
DTO Data Transfer Object — proto request / response. DTO Data Transfer Object — hand-written HTTP request / response.
DO Domain Object — pure biz model, no proto, no storage tags. DO Domain Object — pure biz model, no proto, no storage tags.
PO Persistent Object — storage shape, owned by `data`. PO Persistent Object — storage shape, owned by `data`.
``` ```
@ -39,11 +46,13 @@ owns the PO; `service` is a pass-through that converts at its boundary.
| biz | DO | DO | DTO, PO, storage client | | biz | DO | DO | DTO, PO, storage client |
| data | PO | DO ↔ PO | DTO | | data | PO | DO ↔ PO | DTO |
- `service` imports `api/...` (DTO) and `biz` (DO). Never `data`. - `service` imports `internal/service/dto` (DTO) and `biz` (DO). Never `data`.
- `biz` imports `api/...` only for error reason enums. Never `service`, - `biz` never imports `service` or `data`. The repo interface declared here is
never `data`. The repo interface declared here is the inversion seam. the inversion seam.
- `data` imports `biz` to implement the repo interface. Never `service`, - `data` imports `biz` to implement the repo interface. Never `service`,
never DTOs. never DTOs.
- `integration` implements external I/O boundaries and may import `biz` and
provider SDKs; it is not a utility layer.
- `cmd` is the only place that wires all layers via Wire. - `cmd` is the only place that wires all layers via Wire.
A change crossing these arrows the wrong way is a layering bug; fix the A change crossing these arrows the wrong way is a layering bug; fix the

View File

@ -26,9 +26,9 @@
|----|--------|------| |----|--------|------|
| ~~biz 接口+data 实现~~ | ~~`RecordDataAccess` 写入链已删除;查询侧仍保留。~~ | ~~相关文件~~ | | ~~biz 接口+data 实现~~ | ~~`RecordDataAccess` 写入链已删除;查询侧仍保留。~~ | ~~相关文件~~ |
| ~~service 包装~~ | ~~security_session.go 中 8 个无消费者透传方法已删除。~~ | ~~相关文件~~ | | ~~service 包装~~ | ~~security_session.go 中 8 个无消费者透传方法已删除。~~ | ~~相关文件~~ |
| biz 注入面 | `RegisterBusinessModule`W-1 的成因之一,两阶段注册+回滚补偿零调用);`PaymentBusinessModule`/`PayInternal`/`RefundInternal`/`AuthorizeRefund` 接口面仍无生产实现biz 调用链真实存在,仅实现者缺——与 W-1 一并处理) | biz/payment/payment.go:400-412payment_order.go:139-152 | | ~~biz 注入面~~ | ~~经复核保留:`RegisterBusinessModule` 与 `PaymentBusinessModule` 是 `docs/PAYMENT.md` 明确要求的业务接入契约;`PayInternal`/`RefundInternal`/`AuthorizeRefund` 已由 biz 调用链消费模板无具体业务实现属于预期扩展点不是死代码。评估完成保留2026-08-28。~~ | ~~biz/payment/payment.go:400-412payment_order.go:139-152~~ |
| ~~dto 死字段(已确认项)~~ | ~~GetAuthorityButtonsRequest.Selected 已删除;其余字段因仍参与响应或兼容契约暂保留。~~ | ~~dto/permission.go~~ | | ~~dto 死字段(已确认项)~~ | ~~GetAuthorityButtonsRequest.Selected 已删除;其余字段因仍参与响应或兼容契约暂保留。~~ | ~~dto/permission.go~~ |
| 死分支 | export_excel.go 的 `case []byte` 在 data 层按列类型转换R-2 修复)后成为死分支 | service/system/export_excel.go:85-86 | | ~~死分支~~ | ~~经复核保留service 层接收通用 `map[string]any``[]byte` 仍是合法 usecase/fake 输入该分支有针对性测试不属于可证明死代码。评估完成保留2026-08-28。~~ | ~~service/system/export_excel.go:85-86~~ |
### 2.2 mq / websocket 零消费者基础设施【既定排除范围,历轮明确不处理,现状保持】 ### 2.2 mq / websocket 零消费者基础设施【既定排除范围,历轮明确不处理,现状保持】
@ -40,12 +40,12 @@
| 死代码 | 位置 | 证据 | | 死代码 | 位置 | 证据 |
|--------|------|------| |--------|------|------|
| `config.CloneData` | internal/config/clone.go:8 | 全仓零调用(其余 Clone* 均有生产调用) | | ~~`config.CloneData`~~ | ~~已删除:全仓零调用,且 `Clone`/`MergeRuntimeConfig` 已覆盖实际快照复制入口。已完成2026-08-28。~~ | ~~internal/config/clone.go~~ |
| `paymentkit.XMLValues`/`XMLEncode` | internal/paymentkit/xml.go:16,41 | 仅测试调用,生产 XML 走 gopay 库 | | ~~`paymentkit.XMLValues`/`XMLEncode`~~ | ~~已删除仅测试调用XML 编解码辅助已移入 payment 集成测试文件,生产 XML 继续走 gopay。已完成2026-08-28。~~ | ~~internal/paymentkit/xml.go~~ |
| `paymentkit.NestedString` | internal/paymentkit/json.go:18 | 生产+测试均零调用(旧 shim 删除后的孤儿) | | ~~`paymentkit.NestedString`~~ | ~~已删除:生产零调用;旧测试已改为覆盖现存 `JSONObject`/`StringAtPath`。已完成2026-08-28。~~ | ~~internal/paymentkit/json.go~~ |
| `logging.NewZapLogger` | internal/logging/zap.go:542 | 仅测试调用,生产用 NewReloadableZapLogger | | `logging.NewZapLogger` | internal/logging/zap.go:542 | 仅测试调用,生产用 NewReloadableZapLogger |
| ~~`data/payment.contains`~~ | ~~已改用 `paymentkit.ContainsFold`,本地实现已删除。~~ | ~~data/payment/payment.go~~ | | ~~`data/payment.contains`~~ | ~~已改用 `paymentkit.ContainsFold`,本地实现已删除。~~ | ~~data/payment/payment.go~~ |
| `paymentkit status.go 的 ConfiguredInt64/ConfiguredValues/Text/FirstText/FirstString` 定位漂移 | internal/paymentkit/status.go:36-90 | 属"供应商配置解析"超出 README 声称范围(文档漂移,非死代码) | | ~~`paymentkit status.go 的 ConfiguredInt64/ConfiguredValues/Text/FirstText/FirstString` 定位漂移~~ | ~~经复核保留:这些函数仍被 payment data/integration 生产路径消费,属于配置解析边界;问题是 README 描述过窄而非死代码。评估完成保留2026-08-28。~~ | ~~internal/paymentkit/status.go:36-90~~ |
## 三、重复实现 / 双份维护 ## 三、重复实现 / 双份维护
@ -55,7 +55,7 @@
|---|------|------|------| |---|------|------|------|
| ~~D-2部分完成~~ | ~~支付方式归一化骨架已统一到 `paymentkit.NormalizePaymentMethod`;各渠道状态词表与退款身份校验因语义不同保留。~~ | ~~integration/paymentinternal/paymentkit~~ | ~~部分完成~~ | | ~~D-2部分完成~~ | ~~支付方式归一化骨架已统一到 `paymentkit.NormalizePaymentMethod`;各渠道状态词表与退款身份校验因语义不同保留。~~ | ~~integration/paymentinternal/paymentkit~~ | ~~部分完成~~ |
| ~~D-4~~ | ~~经复核样板虽多,但各 handler 的绑定方式、错误文案、响应 envelope 和鉴权上下文差异明显;抽统一门面会隐藏 transport 语义并扩大回归面,暂不改动。~~ | ~~server/handler/*~~ | ~~评估完成保留2026-08-28~~ | | ~~D-4~~ | ~~经复核样板虽多,但各 handler 的绑定方式、错误文案、响应 envelope 和鉴权上下文差异明显;抽统一门面会隐藏 transport 语义并扩大回归面,暂不改动。~~ | ~~server/handler/*~~ | ~~评估完成保留2026-08-28~~ |
| D-6 | mq/websocket 两包各写一套 map 解码 helper 且逐字符相同TestConfig 探测骨架三处同构【属 2.2 排除范围交叉项】 | emqx.go:226-260 vs websocket/server.go:189-240 | 二轮 | | ~~D-6~~ | ~~经复核暂不改动:重复 helper 与 MQ/WebSocket 零消费者基础设施同属既定排除范围;上收 `internal/utils` 会扩大无消费者包的公共表面积。~~ | ~~emqx.go:226-260 vs websocket/server.go:189-240~~ | ~~评估完成保留2026-08-28~~ |
### 3.2 配置/数据不变式双份维护 ### 3.2 配置/数据不变式双份维护
@ -95,9 +95,9 @@
| # | 问题 | 位置 | 轮次 | | # | 问题 | 位置 | 轮次 |
|---|------|------|------| |---|------|------|------|
| ~~S-1第一批~~ | ~~已合并 actor/data-scope 上下文载荷文件;其余微文件仍待按职责归并。~~ | ~~internal/biz/system/context.go~~ | ~~部分完成~~ | | ~~S-1第一批~~ | ~~已合并 actor/data-scope 上下文载荷文件;复核剩余微文件均对应独立职责,不再机械归并。~~ | ~~internal/biz/system/context.go~~ | ~~部分完成2026-08-28~~ |
| ~~S-2~~ | ~~SystemConfigService 已合并为单文件。~~ | ~~internal/service/system/system.go~~ | ~~已完成~~ | | ~~S-2~~ | ~~SystemConfigService 已合并为单文件。~~ | ~~internal/service/system/system.go~~ | ~~已完成~~ |
| ~~S-3第一批~~ | ~~routes.go 已改为有序注册表;各领域路由文件保留。~~ | ~~internal/server/router/routes.go~~ | ~~部分完成~~ | | ~~S-3第一批~~ | ~~routes.go 已改为有序注册表;各领域路由文件承担独立注册边界,复核后保留。~~ | ~~internal/server/router/routes.go~~ | ~~部分完成2026-08-28~~ |
| ~~S-4~~ | ~~四个 data 子包的 provider 文件已合并。~~ | ~~internal/data/*/provider.go~~ | ~~已完成~~ | | ~~S-4~~ | ~~四个 data 子包的 provider 文件已合并。~~ | ~~internal/data/*/provider.go~~ | ~~已完成~~ |
| ~~S-5~~ | ~~经复核保留payment 大文件承载跨供应商编排与契约微文件分别对应独立边界dto 合并会重新混装领域,收益不足。~~ | ~~biz/payment、service/dto~~ | ~~评估完成保留2026-08-28~~ | | ~~S-5~~ | ~~经复核保留payment 大文件承载跨供应商编排与契约微文件分别对应独立边界dto 合并会重新混装领域,收益不足。~~ | ~~biz/payment、service/dto~~ | ~~评估完成保留2026-08-28~~ |
| ~~S-6~~ | ~~经复核保留:`system.go`/`settings.go` 虽跨域,但移动类型会放大 service DTO 导入与生成契约变化;本批不做机械拆分。~~ | ~~service/dto/system.go、settings.go~~ | ~~评估完成保留2026-08-28~~ | | ~~S-6~~ | ~~经复核保留:`system.go`/`settings.go` 虽跨域,但移动类型会放大 service DTO 导入与生成契约变化;本批不做机械拆分。~~ | ~~service/dto/system.go、settings.go~~ | ~~评估完成保留2026-08-28~~ |
@ -120,7 +120,7 @@
| P-13 | pkg/module、pkg/task、pkg/database/migration 同层契约组维持现状 | pkg/* | 一轮 | | P-13 | pkg/module、pkg/task、pkg/database/migration 同层契约组维持现状 | pkg/* | 一轮 |
| P-14 | httpx 移动后业务语义未剥离CodePasswordChangeRequired=10001 与 x-token cookie 仍留在 internal/server/httpxSetTokenCookie 注释自称 "no KRA business dependency" 与语义不符 | internal/server/httpx/response.go:17,58-64 | 四轮P-1 移动残留) | | P-14 | httpx 移动后业务语义未剥离CodePasswordChangeRequired=10001 与 x-token cookie 仍留在 internal/server/httpxSetTokenCookie 注释自称 "no KRA business dependency" 与语义不符 | internal/server/httpx/response.go:17,58-64 | 四轮P-1 移动残留) |
| ~~P-15~~ | ~~包移动注释与 logging 栈跳过标记已修正。~~ | ~~相关文件~~ | ~~已完成~~ | | ~~P-15~~ | ~~包移动注释与 logging 栈跳过标记已修正。~~ | ~~相关文件~~ | ~~已完成~~ |
| P-16 | CLAUDE.md 结构描述整体过时(描述 api/、internal/global/ 等不存在目录),与 AGENTS.md 不同步 | CLAUDE.md:9-17 | 四轮 | | ~~P-16~~ | ~~已同步 CLAUDE.md 的目录结构与分层说明,移除不存在的 `api/`、`internal/global/` 等描述。~~ | ~~CLAUDE.md~~ | ~~已完成2026-08-28~~ |
## 七、分层 / 职责违规 ## 七、分层 / 职责违规
@ -132,7 +132,7 @@
| ~~L-6部分完成~~ | ~~`AuthenticationResult` 已在返回 service 前清空密码哈希并补回归测试。~~ `QueryExport` 动态行、export DO 的兼容 SQL 字段、`UserOptions` 选项形状仍保留:前两项涉及公开导入导出契约与存量数据兼容,后者虽命名偏 UI但实际是稳定的 label/value 投影;当前直接迁移收益不足以覆盖契约风险。 | biz/system/* | ~~部分完成2026-08-28针对性测试通过~~ | | ~~L-6部分完成~~ | ~~`AuthenticationResult` 已在返回 service 前清空密码哈希并补回归测试。~~ `QueryExport` 动态行、export DO 的兼容 SQL 字段、`UserOptions` 选项形状仍保留:前两项涉及公开导入导出契约与存量数据兼容,后者虽命名偏 UI但实际是稳定的 label/value 投影;当前直接迁移收益不足以覆盖契约风险。 | biz/system/* | ~~部分完成2026-08-28针对性测试通过~~ |
| L-7 | data 层纪律Table("字符串") 绕过 POsaveRelations 回写入参 DOOriginSetting 裸转换 | data/system/* | 二轮 | | L-7 | data 层纪律Table("字符串") 绕过 POsaveRelations 回写入参 DOOriginSetting 裸转换 | data/system/* | 二轮 |
| L-8 | 编排类文件过重seedSystem 126 行 10 类职责authority.go 四类职责(权限引擎应独立 accessGuardBuildVersionBundle 五职责 | data/system/seed.go、authority.go、version.go | 二轮 | | L-8 | 编排类文件过重seedSystem 126 行 10 类职责authority.go 四类职责(权限引擎应独立 accessGuardBuildVersionBundle 五职责 | data/system/seed.go、authority.go、version.go | 二轮 |
| ~~L-9部分完成~~ | ~~`AuthorityResponse.DeletedAt` 已改为 `json:"-"`不再泄漏软删字段。~~ ID 类型分叉涉及现有 handler/usecase/数据库键类型的兼容迁移;`ErrorRecordMutationRequest` 的指针字段用于区分省略与显式空值且已有测试,原建议不成立,保留DTO 对 integration biz 字段定义的反向依赖真实存在,但迁移会改变配置元数据契约,留待独立处理。 | service/dto/* | ~~部分完成2026-08-28针对性测试通过~~ | | ~~L-9部分完成~~ | ~~`AuthorityResponse.DeletedAt` 已改为 `json:"-"`且 integration 配置字段已迁移为 DTO 自有类型并在 service 边界映射。~~ ID 类型分叉涉及现有 handler/usecase/数据库键类型的兼容迁移;`ErrorRecordMutationRequest` 的指针字段用于区分省略与显式空值且已有测试,原建议不成立,保留。 | ~~service/dto/*~~ | ~~部分完成2026-08-28针对性测试通过~~ |
| L-10 | 错误体系双轨errors.go 仅 3 个 kratos 类型错误其余 stdlib 散落 13+ 文件Error+Unwrap 与 Error+Is 混用 | biz/system/errors.go 等 | 二轮 | | L-10 | 错误体系双轨errors.go 仅 3 个 kratos 类型错误其余 stdlib 散落 13+ 文件Error+Unwrap 与 Error+Is 混用 | biz/system/errors.go 等 | 二轮 |
| L-12 | 校验双轨制handler 手工 if 与 dto binding 标签混用 | server/handler/* + service/dto/* | 一/三轮 | | L-12 | 校验双轨制handler 手工 if 与 dto binding 标签混用 | server/handler/* + service/dto/* | 一/三轮 |

View File

@ -5,7 +5,6 @@ package config
// serializer to hand out a snapshot a caller may safely mutate. Each function // serializer to hand out a snapshot a caller may safely mutate. Each function
// returns nil for a nil input. // returns nil for a nil input.
func Clone(value *Config) *Config { return cloneConfig(value) } func Clone(value *Config) *Config { return cloneConfig(value) }
func CloneData(value *Data) *Data { return cloneData(value) }
func CloneAdmin(value *Admin) *Admin { return cloneAdmin(value) } func CloneAdmin(value *Admin) *Admin { return cloneAdmin(value) }
func CloneDatabase(value *Database) *Database { return clonePtr(value) } func CloneDatabase(value *Database) *Database { return clonePtr(value) }
func CloneStorage(value *Storage) *Storage { return cloneStorage(value) } func CloneStorage(value *Storage) *Storage { return cloneStorage(value) }

View File

@ -21,8 +21,6 @@ import (
"strings" "strings"
"testing" "testing"
"kra/internal/paymentkit"
gopayAlipay "github.com/go-pay/gopay/alipay" gopayAlipay "github.com/go-pay/gopay/alipay"
gopayDouyin "github.com/go-pay/gopay/douyin" gopayDouyin "github.com/go-pay/gopay/douyin"
gopayWechat "github.com/go-pay/gopay/wechat" gopayWechat "github.com/go-pay/gopay/wechat"
@ -177,7 +175,7 @@ func TestWechatV2GoPayCallbackVerificationAndSandboxAmount(t *testing.T) {
t.Errorf("read request: %v", err) t.Errorf("read request: %v", err)
return return
} }
requestValues, err = paymentkit.XMLValues(body) requestValues, err = testXMLValues(body)
if err != nil { if err != nil {
t.Errorf("parse request: %v", err) t.Errorf("parse request: %v", err)
return return
@ -190,7 +188,7 @@ func TestWechatV2GoPayCallbackVerificationAndSandboxAmount(t *testing.T) {
} }
response := map[string]string{"return_code": "SUCCESS", "return_msg": "OK", "result_code": "SUCCESS", "prepay_id": "prepay"} response := map[string]string{"return_code": "SUCCESS", "return_msg": "OK", "result_code": "SUCCESS", "prepay_id": "prepay"}
response["sign"] = wechatV2Sign(response, key, "MD5") response["sign"] = wechatV2Sign(response, key, "MD5")
_, _ = w.Write(paymentkit.XMLEncode(response)) _, _ = w.Write(testXMLEncode(response))
})) }))
defer server.Close() defer server.Close()
@ -219,7 +217,7 @@ func TestWechatV2GoPayCallbackVerificationAndSandboxAmount(t *testing.T) {
if callbackValues["sign"] != originalSign { if callbackValues["sign"] != originalSign {
t.Fatal("verification mutated source values") t.Fatal("verification mutated source values")
} }
callbackBody := paymentkit.XMLEncode(callbackValues) callbackBody := testXMLEncode(callbackValues)
callbackResult, err := (&wechatV2Adapter{}).Callback(context.Background(), &bizpayment.PaymentCallback{Body: callbackBody}, map[string]any{ callbackResult, err := (&wechatV2Adapter{}).Callback(context.Background(), &bizpayment.PaymentCallback{Body: callbackBody}, map[string]any{
"app_id": "wx-test", "merchant_id": "mch-test", "mch_key": key, "app_id": "wx-test", "merchant_id": "mch-test", "mch_key": key,
}) })
@ -243,7 +241,7 @@ func TestWechatV2MicropayUsesGoPaySDKAndValidatesIdentities(t *testing.T) {
t.Errorf("read request: %v", err) t.Errorf("read request: %v", err)
return return
} }
requestValues, err = paymentkit.XMLValues(body) requestValues, err = testXMLValues(body)
if err != nil { if err != nil {
t.Errorf("parse request: %v", err) t.Errorf("parse request: %v", err)
return return
@ -263,7 +261,7 @@ func TestWechatV2MicropayUsesGoPaySDKAndValidatesIdentities(t *testing.T) {
"transaction_id": "WX-TRANSACTION-1", "total_fee": "123", "cash_fee": "123", "fee_type": "CNY", "transaction_id": "WX-TRANSACTION-1", "total_fee": "123", "cash_fee": "123", "fee_type": "CNY",
} }
response["sign"] = wechatV2Sign(response, key, "MD5") response["sign"] = wechatV2Sign(response, key, "MD5")
_, _ = w.Write(paymentkit.XMLEncode(response)) _, _ = w.Write(testXMLEncode(response))
})) }))
defer server.Close() defer server.Close()
@ -344,7 +342,7 @@ func TestWechatV2MicropayRejectsInvalidSuccessResponse(t *testing.T) {
} }
tc.mutate(response) tc.mutate(response)
response["sign"] = wechatV2Sign(response, key, "MD5") response["sign"] = wechatV2Sign(response, key, "MD5")
_, _ = w.Write(paymentkit.XMLEncode(response)) _, _ = w.Write(testXMLEncode(response))
})) }))
defer server.Close() defer server.Close()

View File

@ -12,8 +12,6 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"kra/internal/paymentkit"
"github.com/go-pay/gopay" "github.com/go-pay/gopay"
gopayQQ "github.com/go-pay/gopay/qq" gopayQQ "github.com/go-pay/gopay/qq"
) )
@ -55,7 +53,7 @@ func TestQQCreateKeepsClientPaymentDataOutOfDurableIdentities(t *testing.T) {
http.Error(w, "invalid XML", http.StatusBadRequest) http.Error(w, "invalid XML", http.StatusBadRequest)
return return
} }
values, err := paymentkit.XMLValues(body) values, err := testXMLValues(body)
if err != nil { if err != nil {
t.Errorf("parse request XML: %v", err) t.Errorf("parse request XML: %v", err)
http.Error(w, "invalid XML", http.StatusBadRequest) http.Error(w, "invalid XML", http.StatusBadRequest)
@ -81,7 +79,7 @@ func TestQQCreateKeepsClientPaymentDataOutOfDurableIdentities(t *testing.T) {
for key, value := range response { for key, value := range response {
encoded[key] = fmt.Sprint(value) encoded[key] = fmt.Sprint(value)
} }
_, _ = w.Write(paymentkit.XMLEncode(encoded)) _, _ = w.Write(testXMLEncode(encoded))
})) }))
result, err := (&qqAdapter{}).Create(context.Background(), &bizpayment.PaymentRequest{ result, err := (&qqAdapter{}).Create(context.Background(), &bizpayment.PaymentRequest{

View File

@ -1,4 +1,4 @@
package paymentkit package payment
import ( import (
"bytes" "bytes"
@ -8,12 +8,12 @@ import (
"strings" "strings"
) )
type xmlValue struct { type testXMLValue struct {
XMLName xml.Name XMLName xml.Name
Value string `xml:",chardata"` Value string `xml:",chardata"`
} }
func XMLValues(raw []byte) (map[string]string, error) { func testXMLValues(raw []byte) (map[string]string, error) {
decoder := xml.NewDecoder(bytes.NewReader(raw)) decoder := xml.NewDecoder(bytes.NewReader(raw))
result := map[string]string{} result := map[string]string{}
for { for {
@ -28,7 +28,7 @@ func XMLValues(raw []byte) (map[string]string, error) {
if !ok || start.Name.Local == "xml" { if !ok || start.Name.Local == "xml" {
continue continue
} }
var value xmlValue var value testXMLValue
if err = decoder.DecodeElement(&value, &start); err != nil { if err = decoder.DecodeElement(&value, &start); err != nil {
return nil, err return nil, err
} }
@ -38,7 +38,7 @@ func XMLValues(raw []byte) (map[string]string, error) {
} }
} }
func XMLEncode(values map[string]string) []byte { func testXMLEncode(values map[string]string) []byte {
var b strings.Builder var b strings.Builder
b.WriteString("<xml>") b.WriteString("<xml>")
keys := make([]string, 0, len(values)) keys := make([]string, 0, len(values))

View File

@ -15,14 +15,6 @@ func JSONObject(raw []byte) map[string]any {
return object return object
} }
func NestedString(value any, keys ...string) string {
found := nestedValue(value, keys...)
if found == nil {
return ""
}
return strings.TrimSpace(fmt.Sprint(found))
}
func StringAtPath(value any, path string) string { func StringAtPath(value any, path string) string {
found := ValueAtPath(value, path) found := ValueAtPath(value, path)
if found == nil { if found == nil {
@ -46,21 +38,3 @@ func ValueAtPath(value any, path string) any {
} }
return current return current
} }
func nestedValue(value any, keys ...string) any {
object, ok := value.(map[string]any)
if !ok {
return nil
}
for _, key := range keys {
if found, exists := object[key]; exists && found != nil && fmt.Sprint(found) != "" {
return found
}
}
for _, nestedKey := range []string{"data", "result", "order", "transaction", "response"} {
if found := nestedValue(object[nestedKey], keys...); found != nil {
return found
}
}
return nil
}

View File

@ -2,10 +2,10 @@ package paymentkit
import "testing" import "testing"
func TestNestedAndPathValues(t *testing.T) { func TestJSONObjectAndPathValues(t *testing.T) {
object := JSONObject([]byte(`{"data":{"status":"SUCCESS","amount":100}}`)) object := JSONObject([]byte(`{"data":{"status":"SUCCESS","amount":100}}`))
if got := NestedString(object, "status"); got != "SUCCESS" { if object == nil {
t.Fatalf("NestedString = %q", got) t.Fatal("JSONObject returned nil")
} }
if got := StringAtPath(object, "data.amount"); got != "100" { if got := StringAtPath(object, "data.amount"); got != "100" {
t.Fatalf("StringAtPath = %q", got) t.Fatalf("StringAtPath = %q", got)

View File

@ -2,7 +2,6 @@ package dto
import ( import (
"encoding/json" "encoding/json"
integrationbiz "kra/internal/biz/integration"
) )
type IntegrationConfigRequest struct { type IntegrationConfigRequest struct {
@ -11,12 +10,28 @@ type IntegrationConfigRequest struct {
} }
type IntegrationConfigResponse struct { type IntegrationConfigResponse struct {
Kind string `json:"kind"` Kind string `json:"kind"`
Provider string `json:"provider"` Provider string `json:"provider"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Configured bool `json:"configured"` Configured bool `json:"configured"`
Config json.RawMessage `json:"config"` Config json.RawMessage `json:"config"`
Fields []integrationbiz.IntegrationConfigField `json:"fields"` Fields []IntegrationConfigField `json:"fields"`
}
type IntegrationConfigField struct {
Key string `json:"key"`
Label string `json:"label"`
Type string `json:"type"`
Required bool `json:"required"`
Secret bool `json:"secret"`
Placeholder string `json:"placeholder,omitempty"`
Description string `json:"description,omitempty"`
Options []IntegrationConfigOption `json:"options,omitempty"`
}
type IntegrationConfigOption struct {
Label string `json:"label"`
Value any `json:"value"`
} }

View File

@ -38,7 +38,7 @@ func (s *IntegrationConfigService) List(ctx context.Context, kind string) ([]*dt
enabled = config.Enabled enabled = config.Enabled
delete(byProvider, definition.Provider) delete(byProvider, definition.Provider)
} }
result = append(result, &dto.IntegrationConfigResponse{Kind: definition.Kind, Provider: definition.Provider, Name: definition.Name, Description: definition.Description, Enabled: enabled, Configured: configured, Config: values, Fields: definition.Fields}) result = append(result, &dto.IntegrationConfigResponse{Kind: definition.Kind, Provider: definition.Provider, Name: definition.Name, Description: definition.Description, Enabled: enabled, Configured: configured, Config: values, Fields: integrationFieldsDTO(definition.Fields)})
} }
for _, config := range configs { for _, config := range configs {
if _, exists := byProvider[config.Provider]; !exists { if _, exists := byProvider[config.Provider]; !exists {
@ -59,7 +59,7 @@ func (s *IntegrationConfigService) Find(ctx context.Context, kind, provider stri
if found { if found {
response.Name = definition.Name response.Name = definition.Name
response.Description = definition.Description response.Description = definition.Description
response.Fields = definition.Fields response.Fields = integrationFieldsDTO(definition.Fields)
response.Config = mergeConfigJSON(definition.Defaults, config.Values) response.Config = mergeConfigJSON(definition.Defaults, config.Values)
} }
return response, nil return response, nil
@ -90,3 +90,21 @@ func mergeConfigJSON(defaults map[string]any, raw json.RawMessage) json.RawMessa
encoded, _ := json.Marshal(values) encoded, _ := json.Marshal(values)
return encoded return encoded
} }
func integrationFieldsDTO(fields []integrationbiz.IntegrationConfigField) []dto.IntegrationConfigField {
if fields == nil {
return nil
}
out := make([]dto.IntegrationConfigField, 0, len(fields))
for _, field := range fields {
item := dto.IntegrationConfigField{Key: field.Key, Label: field.Label, Type: field.Type, Required: field.Required, Secret: field.Secret, Placeholder: field.Placeholder, Description: field.Description}
if field.Options != nil {
item.Options = make([]dto.IntegrationConfigOption, 0, len(field.Options))
for _, option := range field.Options {
item.Options = append(item.Options, dto.IntegrationConfigOption{Label: option.Label, Value: option.Value})
}
}
out = append(out, item)
}
return out
}