feat(payment): 新增微信支付宝积分充值与对账

This commit is contained in:
MasterChiefCN committed 2026-09-02 13:03:34 +08:00
1 parent b7fd7a1b61
commit 421a19d189
41 files changed
+6401 -136

No files matched your search

+67 -25
View File
@@ -94,7 +94,7 @@ func newPluginRuntime(dataDir string) (*pluginRuntime, error) {
return nil, fmt.Errorf("create plugin package directory: %w", err)
}
center := &pluginRuntime{registryPath: filepath.Join(dataDir, "plugin_registry.json"), packageDir: packageDir, plugins: make(map[string]pluginRecord)}
if err := center.bootstrapOfficialPlugins(); err != nil {
if err := center.bootstrapBuiltInPlugins(); err != nil {
return nil, err
}
if err := center.reload(); err != nil {
@@ -103,7 +103,7 @@ func newPluginRuntime(dataDir string) (*pluginRuntime, error) {
return center, nil
}
func (c *pluginRuntime) bootstrapOfficialPlugins() error {
func (c *pluginRuntime) bootstrapBuiltInPlugins() error {
stored, err := c.readRegistry()
if err != nil {
return err
@@ -120,7 +120,7 @@ func (c *pluginRuntime) bootstrapOfficialPlugins() error {
if err != nil {
return fmt.Errorf("读取官方插件目录失败:%w", err)
}
officialIDs := make(map[string]struct{}, len(entries)+2)
builtInIDs := make(map[string]struct{}, len(entries)+2)
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(strings.ToLower(entry.Name()), ".yingce-plugin") {
continue
@@ -140,10 +140,10 @@ func (c *pluginRuntime) bootstrapOfficialPlugins() error {
return fmt.Errorf("加载官方插件 %q:%w", pkg.Manifest.Metadata.ID, err)
}
id := pkg.Manifest.Metadata.ID
if _, duplicate := officialIDs[id]; duplicate {
if _, duplicate := builtInIDs[id]; duplicate {
return fmt.Errorf("官方插件 ID %q 重复", id)
}
officialIDs[id] = struct{}{}
builtInIDs[id] = struct{}{}
manifest := pkg.Manifest
record := byID[id]
if len(record.Raw) > 0 {
@@ -169,38 +169,43 @@ func (c *pluginRuntime) bootstrapOfficialPlugins() error {
record.PackagePath, record.PackageSHA256, record.UpdatedAt = packageName, hash, now
byID[id] = record
}
for _, workflow := range bundledWorkflowPluginManifests() {
officialIDs[workflow.Metadata.ID] = struct{}{}
data, err := json.Marshal(workflow)
bundledManifests := append(bundledWorkflowPluginManifests(), bundledPaymentPluginManifests()...)
for _, bundled := range bundledManifests {
builtInIDs[bundled.Metadata.ID] = struct{}{}
data, err := json.Marshal(bundled)
if err != nil {
return fmt.Errorf("encode official workflow plugin %s: %w", workflow.Metadata.ID, err)
return fmt.Errorf("encode built-in plugin %s: %w", bundled.Metadata.ID, err)
}
record := byID[workflow.Metadata.ID]
record := byID[bundled.Metadata.ID]
if len(record.Raw) > 0 {
var installed protocol.Manifest
if err := json.Unmarshal(record.Raw, &installed); err != nil {
return fmt.Errorf("decode bundled workflow plugin %s: %w", workflow.Metadata.ID, err)
return fmt.Errorf("decode built-in plugin %s: %w", bundled.Metadata.ID, err)
}
workflow.Metadata.Enabled = installed.Metadata.Enabled
data, err = json.Marshal(workflow)
bundled.Metadata.Enabled = installed.Metadata.Enabled
data, err = json.Marshal(bundled)
if err != nil {
return fmt.Errorf("encode bundled workflow plugin %s: %w", workflow.Metadata.ID, err)
return fmt.Errorf("encode built-in plugin %s: %w", bundled.Metadata.ID, err)
}
}
now := time.Now().UTC()
if record.InstalledAt.IsZero() {
record.InstalledAt = now
}
source := PluginOriginOfficial
if _, systemPayment := systemPaymentPolicies[bundled.Metadata.ID]; systemPayment {
source = PluginOriginSystem
}
record.UpdatedAt = now
record.ID, record.Raw, record.Source, record.PackagePath = workflow.Metadata.ID, data, PluginOriginOfficial, ""
byID[workflow.Metadata.ID] = record
record.ID, record.Raw, record.Source, record.PackagePath = bundled.Metadata.ID, data, source, ""
byID[bundled.Metadata.ID] = record
}
result := make([]pluginRegistryRecord, 0, len(byID))
for _, record := range byID {
if record.Source == "bundled" || record.Source == PluginOriginOfficial {
if _, exists := officialIDs[record.ID]; !exists {
// Official records are reconciled from repository packages on every
// startup, so removed packages cannot survive as stale providers.
if isBuiltInPluginSource(record.Source) {
if _, exists := builtInIDs[record.ID]; !exists {
// Built-in records are reconciled from repository packages and host
// manifests on every startup, so removed plugins cannot survive stale.
continue
}
}
@@ -210,6 +215,15 @@ func (c *pluginRuntime) bootstrapOfficialPlugins() error {
return c.writeRegistry(result)
}
func isBuiltInPluginSource(source string) bool {
switch strings.TrimSpace(source) {
case "bundled", PluginOriginOfficial, PluginOriginSystem:
return true
default:
return false
}
}
func officialPluginPackageDir() (string, error) {
if configured := strings.TrimSpace(os.Getenv("CANVAS_OFFICIAL_PLUGIN_DIR")); configured != "" {
info, err := os.Stat(configured)
@@ -275,6 +289,31 @@ func (c *pluginRuntime) reload() error {
return err
}
for id, record := range plugins {
var manifest protocol.Manifest
if err := json.Unmarshal(record.Raw, &manifest); err != nil {
record.Error = err.Error()
plugins[id] = record
continue
}
if len(manifest.Contributes.PaymentProviders) > 0 {
_, systemPayment := systemPaymentPolicies[id]
if record.Source != PluginOriginSystem || !systemPayment {
record.Metadata.Enabled = false
record.Status = "invalid"
record.Error = "首期支付插件只能使用系统宿主适配器"
plugins[id] = record
continue
}
}
if len(manifest.Contributes.PaymentProviders) > 0 && len(manifest.Contributes.Providers) == 0 {
if record.Metadata.Enabled {
record.Status = "enabled"
} else {
record.Status = "disabled"
}
plugins[id] = record
continue
}
adapters, loadErr := protocol.LoadInstalledProviders(record.Raw, nil)
if loadErr != nil {
record.Metadata.Enabled = false
@@ -341,6 +380,9 @@ func (c *pluginRuntime) install(data []byte, fileName string) (PluginView, error
return PluginView{}, err
}
manifest := pkg.Manifest
if len(manifest.Contributes.PaymentProviders) > 0 {
return PluginView{}, errors.New("首期支付插件只能使用系统宿主适配器")
}
if strings.HasPrefix(strings.TrimSpace(manifest.Runtime.Backend), "host:") {
return PluginView{}, errors.New("上传插件不能使用宿主内置执行器")
}
@@ -350,8 +392,8 @@ func (c *pluginRuntime) install(data []byte, fileName string) (PluginView, error
c.mu.RLock()
existing, exists := c.plugins[manifest.Metadata.ID]
c.mu.RUnlock()
if exists && existing.Source == PluginOriginOfficial {
return PluginView{}, fmt.Errorf("官方插件 %q 不能通过上传覆盖", manifest.Metadata.ID)
if exists && isBuiltInPluginSource(existing.Source) {
return PluginView{}, fmt.Errorf("内置插件 %q 不能通过上传覆盖", manifest.Metadata.ID)
}
manifest.Metadata.Enabled = !exists || existing.Metadata.Enabled
manifestData, err := json.Marshal(manifest)
@@ -458,7 +500,7 @@ func pluginManifestView(raw []byte, metadata protocol.Metadata, source string) P
return PluginManifestView{
ID: metadata.ID, Name: metadata.Name, Version: metadata.Version, APIVersion: "yingce.plugin/v1", Entry: manifest.Entry, Surfaces: manifest.Surfaces,
Description: metadata.Description, Documentation: metadata.Documentation, Author: metadata.Vendor,
Permissions: manifest.Permissions, Trusted: source == PluginOriginOfficial, Runtime: manifest.Runtime,
Permissions: manifest.Permissions, Trusted: isBuiltInPluginSource(source), Runtime: manifest.Runtime,
Configuration: manifest.Configuration, Contributes: manifest.Contributes,
}
}
@@ -472,8 +514,8 @@ func (c *pluginRuntime) uninstall(id string) error {
if !ok {
return fmt.Errorf("插件 %q 不存在", id)
}
if record.Source == PluginOriginOfficial {
return fmt.Errorf("官方插件 %q 不能卸载,可停用该插件", id)
if isBuiltInPluginSource(record.Source) {
return fmt.Errorf("内置插件 %q 不能卸载,可停用该插件", id)
}
stored, err := c.readRegistry()
if err != nil {