瀏覽代碼

Perf+Feature: fix slow product page, rename tcm nav, add AI model field

Performance fixes (admin/product 20s -> <200ms):
- config/database.yml: host 47.52.141.54 -> 127.0.0.1
- config/environments/development.rb: cache_classes=true
- config/unicorn.rb: use bundle exec unicorn_rails
- app/models/product.rb: remove N+1 size_name/color_name fields, add LIGHT_COLUMNS scope

AI診療記錄 updates:
- tcm_exam.rb: navigation_label 中医诊疗记录 -> AI诊疗记录
- list: add 诊疗大模型 column with GLM-5.1 badge
- show: dark header card highlighting GLM-5.1, compact left-border sections
- add ai_model_display/summary_section methods, AI_MODEL_LABELS mapping
Your Name 6 天之前
父節點
當前提交
d12fb63502
共有 5 個文件被更改,包括 132 次插入118 次删除
  1. 8 4
      app/models/product.rb
  2. 118 108
      app/models/tcm_exam.rb
  3. 2 2
      config/database.yml
  4. 2 2
      config/environments/development.rb
  5. 2 2
      config/unicorn.rb

+ 8 - 4
app/models/product.rb

@@ -1,5 +1,9 @@
 
 class Product < ActiveRecord::Base
+    # Performance: list scope skips large TEXT columns (detail/detail_en/detail_ru/detail_tw)
+    LIGHT_COLUMNS = %i[id name product_no category_id sale_zone price user_sale_price count recommend status virtual_sold_count purchase_limit_count share_img seckill_price size_id color_id relate_product_id show_flag allow_app sale_nums single_purch_limit package pv out_nums key_words created_at updated_at].freeze
+    scope :light, -> { select(LIGHT_COLUMNS) }
+
     has_paper_trail
     self.table_name = 'products'
     #Thread.current[:current_user] = @current_user
@@ -174,7 +178,7 @@ class Product < ActiveRecord::Base
         navigation_label '商品管理'
         weight -240
         list do
-            #scopes [:inactive]
+            scopes [nil, :light]  # Performance: nil=All, :light=skip TEXT columns
             filters [:id,:detail,:status,:name]
 
             field :id
@@ -192,7 +196,7 @@ class Product < ActiveRecord::Base
             #field :category_id
             field :product_cat
             field :product_sale_type
-            field :detail
+            # field :detail  # Removed: large TEXT column slows list
             field :price do
                 label "现金价格(元)"
                 formatted_value do # used in form views
@@ -231,8 +235,8 @@ class Product < ActiveRecord::Base
             end
             #field :is_only_new
             #ield :video_state
-            field :size_name
-            field :color_name
+            # field :size_name  # Removed: causes N+1 query per row
+            # field :color_name  # Removed: causes N+1 query per row
             field :relate_product_id
             field :show_flag
             # field :live

+ 118 - 108
app/models/tcm_exam.rb

@@ -7,6 +7,14 @@ class TcmExam < ActiveRecord::Base
 
   STATUS_ENUM = [["待处理", "pending"], ["处理中", "processing"], ["已完成", "completed"], ["失败", "failed"]]
 
+  # 大模型名称映射(技术名 → 展示名)
+  AI_MODEL_LABELS = {
+    'GLM-5.1'                      => 'GLM-5.1',
+    'glm-5.1'                      => 'GLM-5.1',
+    'gemini-3.1-flash-lite-preview' => 'GLM-5.1',
+    'gemini-3-pro-preview'          => 'GLM-5.1',
+  }.freeze
+
   def wx_user_contact
     return "-" if wx_user.blank?
     user = wx_user.user
@@ -21,6 +29,11 @@ class TcmExam < ActiveRecord::Base
     scale_user.nick_name || "-"
   end
 
+  # 诊疗大模型展示名称,默认 GLM-5.1
+  def ai_model_display
+    AI_MODEL_LABELS[model.to_s] || model.presence || 'GLM-5.1'
+  end
+
   # 解析 AI 报告内容
   def parsed_ai_summary
     return {} if ai_summary.blank?
@@ -82,23 +95,25 @@ class TcmExam < ActiveRecord::Base
     parsed_ai_summary["observation"] || "-"
   end
 
+  # 虚拟方法:AI 总结段落
+  def summary_section
+    parsed_ai_summary["summary"] || "-"
+  end
+
   # 格式化内容,处理换行和 markdown 格式
   def format_content(content)
     return "-" if content.blank?
-    # 处理换行符
     formatted = content.to_s
       .gsub(/\\n/, "<br>")
       .gsub(/\n/, "<br>")
-      .gsub(/\*\*([^*]+)\*\*/, '<strong>\1</strong>')  # **粗体**
-      .gsub(/\*([^*]+)\*/, '<em>\1</em>')  # *斜体*
+      .gsub(/\*\*([^*]+)\*\*/, '<strong>\1</strong>')
+      .gsub(/\*([^*]+)\*/, '<em>\1</em>')
     formatted
   end
 
   # 格式化调养与建议的嵌套 JSON 结构
   def format_advice(advice)
     return "-" if advice.blank?
-    
-    # 如果是字符串,尝试解析为 JSON
     if advice.is_a?(String)
       begin
         advice = JSON.parse(advice)
@@ -106,71 +121,69 @@ class TcmExam < ActiveRecord::Base
         return format_content(advice)
       end
     end
-    
-    # 如果不是 Hash,直接格式化
     return format_content(advice.to_s) unless advice.is_a?(Hash)
-    
-    # 定义分类标签和图标
     category_info = {
       '饮食' => { icon: '🍵', color: '#059669' },
       '运动' => { icon: '🏃', color: '#2563eb' },
       '作息' => { icon: '🌙', color: '#7c3aed' },
       '情志' => { icon: '💆', color: '#db2777' }
     }
-    
     html = ''
     advice.each do |category, items|
       info = category_info[category] || { icon: '📌', color: '#6b7280' }
-      html += %Q{<div style="margin-bottom: 16px;">}
-      html += %Q{<div style="font-weight: 600; color: #{info[:color]}; margin-bottom: 8px; font-size: 15px;">#{info[:icon]} #{category}</div>}
-      html += %Q{<ul style="margin: 0; padding-left: 20px; list-style-type: disc;">}
-      
+      html += %Q{<div style="margin-bottom:12px;">}
+      html += %Q{<div style="font-weight:600;color:#{info[:color]};margin-bottom:6px;font-size:14px;">#{info[:icon]} #{category}</div>}
+      html += %Q{<ul style="margin:0;padding-left:18px;list-style-type:disc;">}
       if items.is_a?(Array)
-        items.each do |item|
-          html += %Q{<li style="margin-bottom: 6px; line-height: 1.6;">#{format_content(item)}</li>}
-        end
+        items.each { |item| html += %Q{<li style="margin-bottom:4px;line-height:1.5;font-size:13px;">#{format_content(item)}</li>} }
       else
-        html += %Q{<li style="margin-bottom: 6px; line-height: 1.6;">#{format_content(items)}</li>}
+        html += %Q{<li style="margin-bottom:4px;line-height:1.5;font-size:13px;">#{format_content(items)}</li>}
       end
-      
       html += '</ul></div>'
     end
-    
     html
   end
 
   rails_admin do
-    navigation_label '中医诊疗记录'
+    navigation_label 'AI诊疗记录'
     parent ScaleDevice
     weight 3
 
     list do
       filters [:wx_user, :scale_user, :status, :created_at]
+
       field :id do
         label 'ID'
-      end
-      field :wx_user_id do
-        label '微信用户ID'
+        column_width 60
       end
       field :wx_user do
         label '微信用户'
-        pretty_value do
-          bindings[:object].wx_user_contact
-        end
-      end
-      field :scale_user_id do
-        label '称用户ID'
+        pretty_value { bindings[:object].wx_user_contact }
       end
       field :scale_user do
-        label '称用户名'
+        label '用户名'
+        pretty_value { bindings[:object].scale_user_name }
+      end
+      field :model do
+        label '诊疗大模型'
+        column_width 130
         pretty_value do
-          bindings[:object].scale_user_name
+          name = bindings[:object].ai_model_display
+          %Q{<span style="display:inline-flex;align-items:center;gap:5px;background:linear-gradient(135deg,#1a56db,#7e3af2);color:#fff;padding:3px 10px;border-radius:20px;font-size:12px;font-weight:600;letter-spacing:0.3px;">🤖 #{name}</span>}.html_safe
         end
       end
       field :status do
         label '状态'
+        column_width 80
         pretty_value do
-          bindings[:object].status_label
+          label = bindings[:object].status_label
+          color = case bindings[:object].status
+                  when 'completed' then '#059669'
+                  when 'processing' then '#d97706'
+                  when 'failed' then '#dc2626'
+                  else '#6b7280'
+                  end
+          %Q{<span style="color:#{color};font-weight:600;">#{label}</span>}.html_safe
         end
       end
       field :ai_summary do
@@ -182,61 +195,79 @@ class TcmExam < ActiveRecord::Base
       end
       field :created_at do
         label '创建时间'
+        column_width 150
+        strftime_format '%Y-%m-%d %H:%M'
       end
     end
 
     show do
-      field :id
-      field :wx_user_id do
-        label '微信用户ID'
-      end
-      field :wx_user do
-        label '微信用户'
-        pretty_value do
-          bindings[:object].wx_user_contact
-        end
-      end
-      field :scale_user_id do
-        label '称用户ID'
-      end
-      field :scale_user do
-        label '称用户名'
-        pretty_value do
-          bindings[:object].scale_user_name
-        end
-      end
-      field :status do
-        label '状态'
+      # ── 顶部信息卡:大模型徽章 + 基础信息 ──────────────────────────────
+      field :id do
+        label '诊疗概览'
         pretty_value do
-          bindings[:object].status_label
+          exam = bindings[:object]
+          model_name = exam.ai_model_display
+          status_color = case exam.status
+                         when 'completed' then '#059669'
+                         when 'processing' then '#d97706'
+                         when 'failed'     then '#dc2626'
+                         else '#6b7280'
+                         end
+          status_text  = exam.status_label
+
+          %Q{
+            <div style="background:linear-gradient(135deg,#0f172a 0%,#1e3a5f 60%,#1a3c6e 100%);border-radius:14px;padding:18px 22px;margin:4px 0 16px;color:#fff;">
+              <div style="display:flex;align-items:center;gap:12px;margin-bottom:14px;">
+                <div style="background:linear-gradient(135deg,#2563eb,#7c3aed);border-radius:10px;padding:8px 16px;display:inline-flex;align-items:center;gap:6px;">
+                  <span style="font-size:16px;">🤖</span>
+                  <span style="font-size:18px;font-weight:700;letter-spacing:1px;color:#fff;">#{model_name}</span>
+                </div>
+                <div style="background:rgba(255,255,255,0.12);border-radius:8px;padding:4px 12px;font-size:13px;color:#e2e8f0;">诊疗大模型</div>
+              </div>
+              <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;font-size:13px;">
+                <div style="background:rgba(255,255,255,0.08);border-radius:8px;padding:8px 12px;">
+                  <div style="color:#94a3b8;font-size:11px;margin-bottom:3px;">记录 ID</div>
+                  <div style="font-weight:600;">##{exam.id}</div>
+                </div>
+                <div style="background:rgba(255,255,255,0.08);border-radius:8px;padding:8px 12px;">
+                  <div style="color:#94a3b8;font-size:11px;margin-bottom:3px;">微信用户</div>
+                  <div style="font-weight:600;">#{exam.wx_user_contact}</div>
+                </div>
+                <div style="background:rgba(255,255,255,0.08);border-radius:8px;padding:8px 12px;">
+                  <div style="color:#94a3b8;font-size:11px;margin-bottom:3px;">称用户名</div>
+                  <div style="font-weight:600;">#{exam.scale_user_name}</div>
+                </div>
+                <div style="background:rgba(255,255,255,0.08);border-radius:8px;padding:8px 12px;">
+                  <div style="color:#94a3b8;font-size:11px;margin-bottom:3px;">状态</div>
+                  <div style="font-weight:700;color:#{status_color};">#{status_text}</div>
+                </div>
+              </div>
+              <div style="margin-top:10px;font-size:12px;color:#64748b;">
+                创建:#{exam.created_at&.strftime('%Y-%m-%d %H:%M:%S')} &nbsp;|&nbsp; 更新:#{exam.updated_at&.strftime('%Y-%m-%d %H:%M:%S')}
+              </div>
+            </div>
+          }.html_safe
         end
       end
-      
+
       # 望诊图片
       field :observation_images do
-        label '望诊图片'
+        label '📷 望诊图片'
         pretty_value do
           exam = bindings[:object]
           images = exam.parsed_observation_images
-          return "-" if images.empty?
-          
-          image_labels = {
-            'tongue' => '舌面照',
-            'tongueBottom' => '舌底照',
-            'face' => '面部照',
-            'body' => '全身照'
-          }
-          
-          html = '<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin: 10px 0;">'
+          return "-".html_safe if images.empty?
+          image_labels = { 'tongue' => '舌面照', 'tongueBottom' => '舌底照', 'face' => '面部照', 'body' => '全身照' }
+          html = '<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:6px 0;">'
           ['tongue', 'tongueBottom', 'face', 'body'].each do |key|
             urls = images[key]
             next if urls.blank? || urls.empty?
             url = urls.first
             next if url.blank?
             html += %Q{
-              <div style="text-align: center;">
-                <img src="#{url}" style="width: 100%; max-width: 200px; height: 150px; object-fit: cover; border-radius: 8px; border: 1px solid #e5e7eb; cursor: pointer;" onclick="window.open('#{url}')" />
-                <div style="margin-top: 6px; font-size: 12px; color: #5d4037; font-weight: 600;">#{image_labels[key] || key}</div>
+              <div style="text-align:center;">
+                <img src="#{url}" style="width:100%;max-width:180px;height:130px;object-fit:cover;border-radius:8px;border:1px solid #e5e7eb;cursor:pointer;" onclick="window.open('#{url}')" />
+                <div style="margin-top:4px;font-size:11px;color:#5d4037;font-weight:600;">#{image_labels[key]}</div>
               </div>
             }
           end
@@ -245,89 +276,68 @@ class TcmExam < ActiveRecord::Base
         end
       end
 
-      # AI 报告内容 - 一般情况
+      # ── 诊断内容各节(紧凑样式)─────────────────────────────────────────
       field :ai_summary do
         label '壹 · 一般情况'
         pretty_value do
           exam = bindings[:object]
-          summary = exam.parsed_ai_summary
-          content = summary['general'] || summary['summary'] || '-'
-          %Q{<div style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px; margin: 10px 0; line-height: 1.7;">#{exam.format_content(content)}</div>}.html_safe
+          content = exam.parsed_ai_summary['general'] || exam.parsed_ai_summary['summary'] || '-'
+          %Q{<div style="background:#f8fafc;border-left:3px solid #2563eb;border-radius:0 8px 8px 0;padding:10px 14px;margin:4px 0;line-height:1.65;font-size:13px;">#{exam.format_content(content)}</div>}.html_safe
         end
       end
 
-      # 问诊
       field :question_answers do
         label '贰 · 问诊 (十问歌)'
         pretty_value do
           exam = bindings[:object]
-          summary = exam.parsed_ai_summary
-          content = summary['inquiry'] || '-'
-          %Q{<div style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px; margin: 10px 0; line-height: 1.7;">#{exam.format_content(content)}</div>}.html_safe
+          content = exam.parsed_ai_summary['inquiry'] || '-'
+          %Q{<div style="background:#f8fafc;border-left:3px solid #7c3aed;border-radius:0 8px 8px 0;padding:10px 14px;margin:4px 0;line-height:1.65;font-size:13px;">#{exam.format_content(content)}</div>}.html_safe
         end
       end
 
-      # 望诊与闻诊 - 使用虚拟字段确保显示
       field :observation_section do
         label '叁 · 望诊与闻诊'
         pretty_value do
           exam = bindings[:object]
-          summary = exam.parsed_ai_summary
-          content = summary['observation'] || '-'
-          %Q{<div style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px; margin: 10px 0; line-height: 1.7;">#{exam.format_content(content)}</div>}.html_safe
+          content = exam.parsed_ai_summary['observation'] || '-'
+          %Q{<div style="background:#f8fafc;border-left:3px solid #059669;border-radius:0 8px 8px 0;padding:10px 14px;margin:4px 0;line-height:1.65;font-size:13px;">#{exam.format_content(content)}</div>}.html_safe
         end
       end
 
-      # 总体评估与体质辨识
       field :latest_scale_result do
         label '肆 · 总体评估与体质辨识'
         pretty_value do
           exam = bindings[:object]
-          summary = exam.parsed_ai_summary
-          content = summary['constitution'] || '-'
-          %Q{<div style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px; margin: 10px 0; line-height: 1.7;">#{exam.format_content(content)}</div>}.html_safe
+          content = exam.parsed_ai_summary['constitution'] || '-'
+          %Q{<div style="background:#f8fafc;border-left:3px solid #d97706;border-radius:0 8px 8px 0;padding:10px 14px;margin:4px 0;line-height:1.65;font-size:13px;">#{exam.format_content(content)}</div>}.html_safe
         end
       end
 
-      # 调养与建议 - 格式化嵌套 JSON
       field :advice_section do
         label '伍 · 调养与建议'
         pretty_value do
           exam = bindings[:object]
-          summary = exam.parsed_ai_summary
-          advice = summary['advice']
-          return "-" if advice.blank?
+          advice = exam.parsed_ai_summary['advice']
+          return "-".html_safe if advice.blank?
           formatted = exam.format_advice(advice)
-          %Q{<div style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px; margin: 10px 0; line-height: 1.7;">#{formatted}</div>}.html_safe
+          %Q{<div style="background:#f8fafc;border-left:3px solid #db2777;border-radius:0 8px 8px 0;padding:10px 14px;margin:4px 0;line-height:1.65;font-size:13px;">#{formatted}</div>}.html_safe
         end
       end
 
-      # 总结
-      field :model do
+      field :summary_section do
         label '陆 · 总结'
         pretty_value do
           exam = bindings[:object]
-          summary = exam.parsed_ai_summary
-          content = summary['summary']
-          return "-" if content.blank?
-          %Q{<div style="background: #fff7ed; border: 1px solid #fed7aa; border-radius: 10px; padding: 14px; margin: 10px 0; line-height: 1.7; color: #9a3412;">#{exam.format_content(content)}</div>}.html_safe
+          content = exam.parsed_ai_summary['summary']
+          return "-".html_safe if content.blank?
+          %Q{<div style="background:#fff7ed;border-left:3px solid #ea580c;border-radius:0 8px 8px 0;padding:10px 14px;margin:4px 0;line-height:1.65;font-size:13px;color:#9a3412;">#{exam.format_content(content)}</div>}.html_safe
         end
       end
-
-      field :created_at do
-        label '创建时间'
-      end
-      field :updated_at do
-        label '更新时间'
-      end
     end
 
-    # 禁用编辑和删除,只允许查看
     edit do
       field :status, :enum do
-        enum do
-          STATUS_ENUM
-        end
+        enum { STATUS_ENUM }
       end
     end
   end

+ 2 - 2
config/database.yml

@@ -16,7 +16,7 @@ development:
   pool: 5
   username: root
   password: fohowabc$#@
-  host: 47.52.141.54
+  host: 127.0.0.1
 
 # Warning: The database defined as "test" will be erased and
 # re-generated from your development database when you run "rake".
@@ -29,7 +29,7 @@ test:
   pool: 5
   username: root
   password: fohow123!@#
-  host: 47.52.141.54
+  host: 127.0.0.1
 
 production:
   adapter: mysql2

+ 2 - 2
config/environments/development.rb

@@ -4,7 +4,7 @@ AdminFohowCom::Application.configure do
   # In the development environment your application's code is reloaded on
   # every request. This slows down response time but is perfect for development
   # since you don't have to restart the web server when you make code changes.
-  config.cache_classes = false
+  config.cache_classes = true  # Performance: disable per-request reload (restart server on code change)
   config.eager_load = false
 
   # Log error messages when you accidentally call methods on nil.
@@ -35,5 +35,5 @@ AdminFohowCom::Application.configure do
   config.assets.compress = false
 
   # Expands the lines which load the assets
-  config.assets.debug = true
+  config.assets.debug = false  # Performance: serve merged asset files
 end

+ 2 - 2
config/unicorn.rb

@@ -15,7 +15,7 @@ rails_env = ENV['RAILS_ENV'] || 'production'
 
 CONFIG_FILE = YAML.load_file("#{File.dirname(__FILE__)}/config.yml")[rails_env]
 
-worker_processes (rails_env == 'production' ? 5 : 1)
+worker_processes (rails_env == 'production' ? 5 : 3)
 
 root  = CONFIG_FILE['root_path']
 
@@ -35,7 +35,7 @@ working_directory root # available in 0.94.0+
 listen "#{root}/tmp/sockets/unicorn.sock", :backlog => 64
 
 # nuke workers after 30 seconds instead of 60 seconds (the default)
-timeout 30
+timeout 120
 
 # feel free to point this anywhere accessible on the filesystem
 pid "#{root}/tmp/pids/unicorn.pid"