Browse Source

fix:AI测量

super-yimizi 3 weeks ago
parent
commit
7f454bbda6

+ 434 - 0
application/admin/controller/BodyProfile.php

@@ -0,0 +1,434 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use app\common\model\BodyProfile as BodyProfileModel;
+use app\common\model\BodyMeasurements;
+use app\common\model\BodyTypeConfig;
+use app\common\model\BodyTypeSelection;
+use app\common\model\BodyAiReport;
+use think\Db;
+use think\exception\ValidateException;
+
+/**
+ * 身体档案管理
+ */
+class BodyProfile extends Backend
+{
+    protected $model = null;
+    protected $noNeedLogin = [];
+    protected $noNeedRight = [];
+    protected $searchFields = 'profile_name,relation';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new BodyProfileModel;
+    }
+
+    /**
+     * 查看列表
+     */
+    public function index()
+    {
+        if ($this->request->isAjax()) {
+            // 分页参数
+            $page = $this->request->get('page/d', 1);
+            $limit = $this->request->get('limit/d', 10);
+            $search = $this->request->get('search', '');
+            $gender = $this->request->get('gender', '');
+            $is_own = $this->request->get('is_own', '');
+
+            $where = [];
+            
+            // 搜索条件
+            if ($search) {
+                $where[] = ['profile_name|relation', 'like', '%' . $search . '%'];
+            }
+            
+            if ($gender !== '') {
+                $where[] = ['gender', '=', $gender];
+            }
+            
+            if ($is_own !== '') {
+                $where[] = ['is_own', '=', $is_own];
+            }
+
+            // 查询数据
+            $list = $this->model
+                ->with(['user', 'latestMeasurement'])
+                ->where($where)
+                ->order('id DESC')
+                ->paginate($limit)
+                ->each(function($item) {
+                    // 添加额外信息
+                    $item['bmi'] = $item->calculateBMI();
+                    $item['bmi_level'] = $item->getBMILevel();
+                    return $item;
+                });
+
+            return json(['code' => 0, 'msg' => '', 'count' => $list->total(), 'data' => $list->items()]);
+        }
+
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加档案
+     */
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $params = $this->request->post('row/a');
+            
+            if (empty($params)) {
+                $this->error(__('Parameter %s can not be empty', ''));
+            }
+
+            // 验证必填字段
+            if (empty($params['profile_name']) || empty($params['user_id'])) {
+                $this->error('档案名称和用户ID不能为空');
+            }
+
+            // 处理身体照片
+            if (isset($params['body_photos']) && is_array($params['body_photos'])) {
+                $params['body_photos'] = json_encode($params['body_photos']);
+            }
+
+            Db::startTrans();
+            try {
+                $result = $this->model->save($params);
+                if ($result === false) {
+                    throw new \Exception($this->model->getError());
+                }
+
+                Db::commit();
+            } catch (\Throwable $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+
+            $this->success();
+        }
+
+        // 获取用户列表
+        $userList = \app\common\model\User::field('id,username,nickname')->select();
+        $this->assign('userList', $userList);
+        
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑档案
+     */
+    public function edit($ids = null)
+    {
+        $row = $this->model->get($ids);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+
+        if ($this->request->isPost()) {
+            $params = $this->request->post('row/a');
+            
+            if (empty($params)) {
+                $this->error(__('Parameter %s can not be empty', ''));
+            }
+
+            // 处理身体照片
+            if (isset($params['body_photos']) && is_array($params['body_photos'])) {
+                $params['body_photos'] = json_encode($params['body_photos']);
+            }
+
+            Db::startTrans();
+            try {
+                $result = $row->save($params);
+                if ($result === false) {
+                    throw new \Exception($row->getError());
+                }
+
+                Db::commit();
+            } catch (\Throwable $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+
+            $this->success();
+        }
+
+        // 获取用户列表
+        $userList = \app\common\model\User::field('id,username,nickname')->select();
+        $this->assign('userList', $userList);
+        $this->assign('row', $row);
+        
+        return $this->view->fetch();
+    }
+
+    /**
+     * 删除档案
+     */
+    public function del($ids = null)
+    {
+        if (!$this->request->isPost()) {
+            $this->error(__("Invalid parameters"));
+        }
+
+        $ids = $ids ? $ids : $this->request->post("ids");
+        if (empty($ids)) {
+            $this->error(__('Parameter %s can not be empty', 'ids'));
+        }
+
+        $pk = $this->model->getPk();
+        $adminIds = $this->getDataLimitAdminIds();
+        if (is_array($adminIds)) {
+            $this->model->where($this->dataLimitField, 'in', $adminIds);
+        }
+
+        $list = $this->model->where($pk, 'in', $ids)->select();
+        $count = 0;
+
+        Db::startTrans();
+        try {
+            foreach ($list as $item) {
+                // 删除相关数据
+                BodyMeasurements::where('profile_id', $item->id)->delete();
+                BodyTypeSelection::where('profile_id', $item->id)->delete();
+                BodyAiReport::where('profile_id', $item->id)->delete();
+                
+                $count += $item->delete();
+            }
+            Db::commit();
+        } catch (\Throwable $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+
+        $this->success();
+    }
+
+    /**
+     * 查看档案详情
+     */
+    public function detail($ids = null)
+    {
+        $profile = $this->model->with(['user', 'latestMeasurement', 'bodyTypeSelections.typeConfig', 'latestAiReport'])->find($ids);
+        if (!$profile) {
+            $this->error('档案不存在');
+        }
+
+        // 获取完整档案数据
+        $profileData = $profile->getFullProfileData();
+        
+        $this->assign('profile', $profileData);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 测量数据管理
+     */
+    public function measurements($profile_id = null)
+    {
+        if (!$profile_id) {
+            $this->error('档案ID不能为空');
+        }
+
+        $profile = $this->model->find($profile_id);
+        if (!$profile) {
+            $this->error('档案不存在');
+        }
+
+        if ($this->request->isAjax()) {
+            // 获取测量记录
+            $list = BodyMeasurements::where('profile_id', $profile_id)
+                ->order('measurement_date DESC')
+                ->paginate(10);
+
+            return json(['code' => 0, 'msg' => '', 'count' => $list->total(), 'data' => $list->items()]);
+        }
+
+        $this->assign('profile', $profile);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加测量数据
+     */
+    public function addMeasurement($profile_id = null)
+    {
+        if (!$profile_id) {
+            $this->error('档案ID不能为空');
+        }
+
+        $profile = $this->model->find($profile_id);
+        if (!$profile) {
+            $this->error('档案不存在');
+        }
+
+        if ($this->request->isPost()) {
+            $params = $this->request->post('row/a');
+            
+            if (empty($params)) {
+                $this->error(__('Parameter %s can not be empty', ''));
+            }
+
+            $params['profile_id'] = $profile_id;
+            
+            // 处理测量日期
+            if (isset($params['measurement_date']) && $params['measurement_date']) {
+                $params['measurement_date'] = strtotime($params['measurement_date']);
+            } else {
+                $params['measurement_date'] = time();
+            }
+
+            $measurement = new BodyMeasurements();
+            $result = $measurement->save($params);
+            
+            if ($result === false) {
+                $this->error($measurement->getError());
+            }
+
+            $this->success();
+        }
+
+        // 获取测量字段
+        $measurementFields = BodyMeasurements::getMeasurementFields($profile->gender);
+        
+        $this->assign('profile', $profile);
+        $this->assign('measurementFields', $measurementFields);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 体型选择管理
+     */
+    public function bodyTypes($profile_id = null)
+    {
+        if (!$profile_id) {
+            $this->error('档案ID不能为空');
+        }
+
+        $profile = $this->model->find($profile_id);
+        if (!$profile) {
+            $this->error('档案不存在');
+        }
+
+        if ($this->request->isPost()) {
+            $selections = $this->request->post('selections/a');
+            
+            if (empty($selections)) {
+                $this->error('请选择体型');
+            }
+
+            $result = BodyTypeSelection::saveUserSelections($profile_id, $selections);
+            
+            if (!$result) {
+                $this->error('保存失败');
+            }
+
+            $this->success();
+        }
+
+        // 获取所有体型分类和选项
+        $bodyTypeCategories = BodyTypeConfig::getAllCategories($profile->gender);
+        
+        // 获取用户已选择的体型
+        $userSelections = BodyTypeSelection::getUserSelections($profile_id);
+
+        $this->assign('profile', $profile);
+        $this->assign('bodyTypeCategories', $bodyTypeCategories);
+        $this->assign('userSelections', $userSelections);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 生成AI报告
+     */
+    public function generateReport($profile_id = null)
+    {
+        if (!$profile_id) {
+            $this->error('档案ID不能为空');
+        }
+
+        $profile = $this->model->find($profile_id);
+        if (!$profile) {
+            $this->error('档案不存在');
+        }
+
+        $report = BodyAiReport::generateReport($profile_id);
+        
+        if (!$report) {
+            $this->error('生成报告失败');
+        }
+
+        $this->success('报告生成成功', null, ['report_id' => $report->id]);
+    }
+
+    /**
+     * 查看AI报告
+     */
+    public function viewReport($report_id = null)
+    {
+        if (!$report_id) {
+            $this->error('报告ID不能为空');
+        }
+
+        $report = BodyAiReport::with(['profile'])->find($report_id);
+        if (!$report) {
+            $this->error('报告不存在');
+        }
+
+        $this->assign('report', $report);
+        return $this->view->fetch();
+    }
+
+    /**
+     * AI测量页面
+     */
+    public function aiMeasurement($profile_id = null)
+    {
+        if (!$profile_id) {
+            $this->error('档案ID不能为空');
+        }
+
+        $profile = $this->model->with(['user'])->find($profile_id);
+        if (!$profile) {
+            $this->error('档案不存在');
+        }
+
+        $this->assign('profile', $profile);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 统计数据
+     */
+    public function statistics()
+    {
+        // 档案统计
+        $profileStats = [
+            'total' => $this->model->count(),
+            'male' => $this->model->where('gender', 1)->count(),
+            'female' => $this->model->where('gender', 2)->count(),
+            'own' => $this->model->where('is_own', 1)->count(),
+            'others' => $this->model->where('is_own', 0)->count(),
+        ];
+
+        // BMI分布统计
+        $bmiStats = [];
+        $profiles = $this->model->where('height', '>', 0)->where('weight', '>', 0)->select();
+        foreach ($profiles as $profile) {
+            $bmi = $profile->calculateBMI();
+            $level = $profile->getBMILevel();
+            $bmiStats[$level] = isset($bmiStats[$level]) ? $bmiStats[$level] + 1 : 1;
+        }
+
+        // 体型选择统计
+        $bodyTypeStats = BodyTypeSelection::getSelectionStatistics();
+
+        $this->assign('profileStats', $profileStats);
+        $this->assign('bmiStats', $bmiStats);
+        $this->assign('bodyTypeStats', $bodyTypeStats);
+        return $this->view->fetch();
+    }
+}

+ 407 - 0
application/admin/controller/BodyTypeConfig.php

@@ -0,0 +1,407 @@
+<?php
+
+namespace app\admin\controller;
+
+use app\common\controller\Backend;
+use app\common\model\BodyTypeConfig as BodyTypeConfigModel;
+use think\Db;
+
+/**
+ * 身体类型配置管理
+ */
+class BodyTypeConfig extends Backend
+{
+    protected $model = null;
+    protected $noNeedLogin = [];
+    protected $noNeedRight = [];
+    protected $searchFields = 'type_name,description';
+
+    public function _initialize()
+    {
+        parent::_initialize();
+        $this->model = new BodyTypeConfigModel;
+    }
+
+    /**
+     * 查看列表
+     */
+    public function index()
+    {
+        if ($this->request->isAjax()) {
+            // 分页参数
+            $page = $this->request->get('page/d', 1);
+            $limit = $this->request->get('limit/d', 10);
+            $search = $this->request->get('search', '');
+            $type_category = $this->request->get('type_category', '');
+            $gender = $this->request->get('gender', '');
+
+            $where = [];
+            
+            // 搜索条件
+            if ($search) {
+                $where[] = ['type_name|description', 'like', '%' . $search . '%'];
+            }
+            
+            if ($type_category !== '') {
+                $where[] = ['type_category', '=', $type_category];
+            }
+            
+            if ($gender !== '') {
+                $where[] = ['gender', '=', $gender];
+            }
+
+            // 查询数据
+            $list = $this->model
+                ->where($where)
+                ->order('type_category ASC, sort ASC, id ASC')
+                ->paginate($limit);
+
+            return json(['code' => 0, 'msg' => '', 'count' => $list->total(), 'data' => $list->items()]);
+        }
+
+        // 获取分类选项
+        $categories = [
+            'shoulder' => '肩型',
+            'chest' => '胸型(男)',
+            'bust' => '胸型(女)',
+            'waist' => '腰型',
+            'hip' => '臀型',
+            'leg' => '腿型'
+        ];
+
+        $this->assign('categories', $categories);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 添加体型配置
+     */
+    public function add()
+    {
+        if ($this->request->isPost()) {
+            $params = $this->request->post('row/a');
+            
+            if (empty($params)) {
+                $this->error(__('Parameter %s can not be empty', ''));
+            }
+
+            // 验证必填字段
+            if (empty($params['type_category']) || empty($params['type_name'])) {
+                $this->error('分类和名称不能为空');
+            }
+
+            // 检查是否重复
+            $exists = $this->model->where([
+                'type_category' => $params['type_category'],
+                'type_name' => $params['type_name'],
+                'gender' => $params['gender'] ?? 0
+            ])->find();
+
+            if ($exists) {
+                $this->error('该体型配置已存在');
+            }
+
+            Db::startTrans();
+            try {
+                $result = $this->model->save($params);
+                if ($result === false) {
+                    throw new \Exception($this->model->getError());
+                }
+
+                Db::commit();
+            } catch (\Throwable $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+
+            $this->success();
+        }
+
+        // 分类选项
+        $categories = [
+            'shoulder' => '肩型',
+            'chest' => '胸型(男)',
+            'bust' => '胸型(女)',
+            'waist' => '腰型',
+            'hip' => '臀型',
+            'leg' => '腿型'
+        ];
+
+        $this->assign('categories', $categories);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 编辑体型配置
+     */
+    public function edit($ids = null)
+    {
+        $row = $this->model->get($ids);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+
+        if ($this->request->isPost()) {
+            $params = $this->request->post('row/a');
+            
+            if (empty($params)) {
+                $this->error(__('Parameter %s can not be empty', ''));
+            }
+
+            // 检查是否重复(排除自己)
+            $exists = $this->model->where([
+                'type_category' => $params['type_category'],
+                'type_name' => $params['type_name'],
+                'gender' => $params['gender'] ?? 0
+            ])->where('id', '<>', $ids)->find();
+
+            if ($exists) {
+                $this->error('该体型配置已存在');
+            }
+
+            Db::startTrans();
+            try {
+                $result = $row->save($params);
+                if ($result === false) {
+                    throw new \Exception($row->getError());
+                }
+
+                Db::commit();
+            } catch (\Throwable $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+
+            $this->success();
+        }
+
+        // 分类选项
+        $categories = [
+            'shoulder' => '肩型',
+            'chest' => '胸型(男)',
+            'bust' => '胸型(女)',
+            'waist' => '腰型',
+            'hip' => '臀型',
+            'leg' => '腿型'
+        ];
+
+        $this->assign('categories', $categories);
+        $this->assign('row', $row);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 删除体型配置
+     */
+    public function del($ids = null)
+    {
+        if (!$this->request->isPost()) {
+            $this->error(__("Invalid parameters"));
+        }
+
+        $ids = $ids ? $ids : $this->request->post("ids");
+        if (empty($ids)) {
+            $this->error(__('Parameter %s can not be empty', 'ids'));
+        }
+
+        $pk = $this->model->getPk();
+        $list = $this->model->where($pk, 'in', $ids)->select();
+        $count = 0;
+
+        Db::startTrans();
+        try {
+            foreach ($list as $item) {
+                // 检查是否有用户选择了该体型
+                $used = \app\common\model\BodyTypeSelection::where('selected_type_id', $item->id)->count();
+                if ($used > 0) {
+                    throw new \Exception("体型「{$item->type_name}」已被用户选择,无法删除");
+                }
+                
+                $count += $item->delete();
+            }
+            Db::commit();
+        } catch (\Throwable $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+
+        $this->success();
+    }
+
+    /**
+     * 批量修改状态
+     */
+    public function multi($ids = null)
+    {
+        if (!$this->request->isPost()) {
+            $this->error(__("Invalid parameters"));
+        }
+
+        $ids = $ids ? $ids : $this->request->post("ids");
+        $action = $this->request->post("action", '');
+
+        if (empty($ids) || empty($action)) {
+            $this->error(__('Parameter %s can not be empty', 'ids/action'));
+        }
+
+        $list = $this->model->where('id', 'in', $ids)->select();
+        $count = 0;
+
+        Db::startTrans();
+        try {
+            foreach ($list as $item) {
+                switch ($action) {
+                    case 'enable':
+                        $item->status = 1;
+                        break;
+                    case 'disable':
+                        $item->status = 0;
+                        break;
+                    default:
+                        break;
+                }
+                $count += $item->save();
+            }
+            Db::commit();
+        } catch (\Throwable $e) {
+            Db::rollback();
+            $this->error($e->getMessage());
+        }
+
+        $this->success();
+    }
+
+    /**
+     * 排序
+     */
+    public function sort()
+    {
+        if ($this->request->isPost()) {
+            $ids = $this->request->post('ids/a');
+            
+            if (empty($ids)) {
+                $this->error('参数错误');
+            }
+
+            Db::startTrans();
+            try {
+                foreach ($ids as $index => $id) {
+                    $this->model->where('id', $id)->update(['sort' => $index + 1]);
+                }
+                Db::commit();
+            } catch (\Throwable $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+
+            $this->success();
+        }
+
+        $type_category = $this->request->get('type_category', '');
+        $where = [];
+        
+        if ($type_category) {
+            $where['type_category'] = $type_category;
+        }
+
+        $list = $this->model->where($where)->order('sort ASC, id ASC')->select();
+
+        $this->assign('list', $list);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 预览体型配置
+     */
+    public function preview($ids = null)
+    {
+        $row = $this->model->get($ids);
+        if (!$row) {
+            $this->error(__('No Results were found'));
+        }
+
+        $this->assign('row', $row);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 导入初始数据
+     */
+    public function import()
+    {
+        if ($this->request->isPost()) {
+            $force = $this->request->post('force', 0);
+            
+            // 检查是否已有数据
+            if (!$force && $this->model->count() > 0) {
+                $this->error('系统中已存在体型配置数据,如需重新导入请勾选强制导入');
+            }
+
+            Db::startTrans();
+            try {
+                if ($force) {
+                    // 清空现有数据
+                    $this->model->where('1=1')->delete();
+                }
+
+                // 导入初始数据
+                $this->importInitialData();
+                
+                Db::commit();
+                $this->success('导入成功');
+            } catch (\Throwable $e) {
+                Db::rollback();
+                $this->error($e->getMessage());
+            }
+        }
+
+        $count = $this->model->count();
+        $this->assign('count', $count);
+        return $this->view->fetch();
+    }
+
+    /**
+     * 导入初始体型数据
+     */
+    private function importInitialData()
+    {
+        $data = [
+            // 肩型
+            ['type_category' => 'shoulder', 'type_name' => '平肩', 'gender' => 0, 'description' => '肩部线条平直,无明显斜度', 'sort' => 1],
+            ['type_category' => 'shoulder', 'type_name' => '溜肩', 'gender' => 0, 'description' => '肩部向下倾斜,线条柔和', 'sort' => 2],
+            ['type_category' => 'shoulder', 'type_name' => '削肩', 'gender' => 0, 'description' => '肩部窄小,线条削瘦', 'sort' => 3],
+
+            // 男性胸型
+            ['type_category' => 'chest', 'type_name' => '平胸', 'gender' => 1, 'description' => '胸部平坦,肌肉不明显', 'sort' => 1],
+            ['type_category' => 'chest', 'type_name' => '肌肉', 'gender' => 1, 'description' => '胸肌发达,线条明显', 'sort' => 2],
+            ['type_category' => 'chest', 'type_name' => '圆胸', 'gender' => 1, 'description' => '胸部丰满圆润', 'sort' => 3],
+
+            // 女性胸型
+            ['type_category' => 'bust', 'type_name' => '小巧', 'gender' => 2, 'description' => '胸部小巧精致', 'sort' => 1],
+            ['type_category' => 'bust', 'type_name' => '正常', 'gender' => 2, 'description' => '胸部大小适中', 'sort' => 2],
+            ['type_category' => 'bust', 'type_name' => '丰满', 'gender' => 2, 'description' => '胸部丰满饱满', 'sort' => 3],
+            ['type_category' => 'bust', 'type_name' => '胖', 'gender' => 2, 'description' => '胸部较大', 'sort' => 4],
+
+            // 腰型
+            ['type_category' => 'waist', 'type_name' => '细腰', 'gender' => 0, 'description' => '腰部纤细,曲线明显', 'sort' => 1],
+            ['type_category' => 'waist', 'type_name' => '正常', 'gender' => 0, 'description' => '腰部比例正常', 'sort' => 2],
+            ['type_category' => 'waist', 'type_name' => '圆腰', 'gender' => 0, 'description' => '腰部较粗,线条圆润', 'sort' => 3],
+
+            // 臀型
+            ['type_category' => 'hip', 'type_name' => '平臀', 'gender' => 0, 'description' => '臀部较平,缺乏曲线', 'sort' => 1],
+            ['type_category' => 'hip', 'type_name' => '正常', 'gender' => 0, 'description' => '臀部曲线正常', 'sort' => 2],
+            ['type_category' => 'hip', 'type_name' => '翘臀', 'gender' => 0, 'description' => '臀部翘挺,曲线优美', 'sort' => 3],
+
+            // 腿型
+            ['type_category' => 'leg', 'type_name' => '直腿', 'gender' => 0, 'description' => '腿部笔直,线条流畅', 'sort' => 1],
+            ['type_category' => 'leg', 'type_name' => 'O型腿', 'gender' => 0, 'description' => '膝盖外弯,呈O型', 'sort' => 2],
+            ['type_category' => 'leg', 'type_name' => 'X型腿', 'gender' => 0, 'description' => '膝盖内弯,呈X型', 'sort' => 3],
+        ];
+
+        foreach ($data as $item) {
+            $item['createtime'] = time();
+            $item['updatetime'] = time();
+            $this->model->create($item);
+        }
+    }
+}

+ 286 - 0
application/admin/view/body_profile/add.html

@@ -0,0 +1,286 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>新增身体档案</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+    <link rel="stylesheet" href="/assets/css/layui.css">
+    <link rel="stylesheet" href="/assets/css/admin.css">
+</head>
+<body>
+    <div class="layui-fluid" style="padding: 15px;">
+        <form class="layui-form" action="{:url('add')}" method="post" lay-filter="profile-form">
+            <div class="layui-row layui-col-space15">
+                <div class="layui-col-md6">
+                    <div class="layui-card">
+                        <div class="layui-card-header">基础信息</div>
+                        <div class="layui-card-body">
+                            <div class="layui-form-item">
+                                <label class="layui-form-label">用户</label>
+                                <div class="layui-input-block">
+                                    <select name="row[user_id]" lay-verify="required">
+                                        <option value="">请选择用户</option>
+                                        {foreach $userList as $user}
+                                        <option value="{$user.id}">{$user.username}({$user.nickname|default='未设置昵称'})</option>
+                                        {/foreach}
+                                    </select>
+                                </div>
+                            </div>
+
+                            <div class="layui-form-item">
+                                <label class="layui-form-label">档案名称</label>
+                                <div class="layui-input-block">
+                                    <input type="text" name="row[profile_name]" lay-verify="required" placeholder="请输入档案名称" autocomplete="off" class="layui-input">
+                                </div>
+                            </div>
+
+                            <div class="layui-form-item">
+                                <label class="layui-form-label">档案类型</label>
+                                <div class="layui-input-block">
+                                    <input type="radio" name="row[is_own]" value="1" title="本人档案" checked>
+                                    <input type="radio" name="row[is_own]" value="0" title="他人档案">
+                                </div>
+                            </div>
+
+                            <div class="layui-form-item">
+                                <label class="layui-form-label">关系</label>
+                                <div class="layui-input-block">
+                                    <input type="text" name="row[relation]" placeholder="如:本人、父亲、母亲、儿子、女儿等" autocomplete="off" class="layui-input">
+                                </div>
+                            </div>
+
+                            <div class="layui-form-item">
+                                <label class="layui-form-label">性别</label>
+                                <div class="layui-input-block">
+                                    <input type="radio" name="row[gender]" value="1" title="男" lay-verify="required">
+                                    <input type="radio" name="row[gender]" value="2" title="女" lay-verify="required">
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="layui-col-md6">
+                    <div class="layui-card">
+                        <div class="layui-card-header">身体数据</div>
+                        <div class="layui-card-body">
+                            <div class="layui-form-item">
+                                <label class="layui-form-label">年龄</label>
+                                <div class="layui-input-block">
+                                    <input type="number" name="row[age]" placeholder="请输入年龄" min="1" max="120" autocomplete="off" class="layui-input">
+                                </div>
+                            </div>
+
+                            <div class="layui-form-item">
+                                <label class="layui-form-label">身高(cm)</label>
+                                <div class="layui-input-block">
+                                    <input type="number" name="row[height]" placeholder="请输入身高(厘米)" min="50" max="250" step="0.1" autocomplete="off" class="layui-input">
+                                </div>
+                            </div>
+
+                            <div class="layui-form-item">
+                                <label class="layui-form-label">体重(kg)</label>
+                                <div class="layui-input-block">
+                                    <input type="number" name="row[weight]" placeholder="请输入体重(公斤)" min="10" max="300" step="0.1" autocomplete="off" class="layui-input">
+                                </div>
+                            </div>
+
+                            <div class="layui-form-item">
+                                <label class="layui-form-label">档案照片</label>
+                                <div class="layui-input-block">
+                                    <div class="layui-upload">
+                                        <button type="button" class="layui-btn" id="uploadProfilePhoto">上传照片</button>
+                                        <div class="layui-upload-list">
+                                            <img class="layui-upload-img" id="profilePhotoPreview" style="width: 100px; height: 100px; display: none;">
+                                            <input type="hidden" name="row[profile_photo]" id="profilePhotoValue">
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="layui-row layui-col-space15" style="margin-top: 15px;">
+                <div class="layui-col-md12">
+                    <div class="layui-card">
+                        <div class="layui-card-header">身体照片</div>
+                        <div class="layui-card-body">
+                            <div class="layui-row layui-col-space15">
+                                <div class="layui-col-md4">
+                                    <div class="layui-form-item">
+                                        <label class="layui-form-label">正面照</label>
+                                        <div class="layui-input-block">
+                                            <div class="layui-upload">
+                                                <button type="button" class="layui-btn layui-btn-sm" id="uploadFrontPhoto">上传正面照</button>
+                                                <div class="layui-upload-list">
+                                                    <img class="layui-upload-img" id="frontPhotoPreview" style="width: 120px; height: 160px; display: none;">
+                                                    <input type="hidden" name="row[body_photos][front]" id="frontPhotoValue">
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <div class="layui-col-md4">
+                                    <div class="layui-form-item">
+                                        <label class="layui-form-label">侧面照</label>
+                                        <div class="layui-input-block">
+                                            <div class="layui-upload">
+                                                <button type="button" class="layui-btn layui-btn-sm" id="uploadSidePhoto">上传侧面照</button>
+                                                <div class="layui-upload-list">
+                                                    <img class="layui-upload-img" id="sidePhotoPreview" style="width: 120px; height: 160px; display: none;">
+                                                    <input type="hidden" name="row[body_photos][side]" id="sidePhotoValue">
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <div class="layui-col-md4">
+                                    <div class="layui-form-item">
+                                        <label class="layui-form-label">背面照</label>
+                                        <div class="layui-input-block">
+                                            <div class="layui-upload">
+                                                <button type="button" class="layui-btn layui-btn-sm" id="uploadBackPhoto">上传背面照</button>
+                                                <div class="layui-upload-list">
+                                                    <img class="layui-upload-img" id="backPhotoPreview" style="width: 120px; height: 160px; display: none;">
+                                                    <input type="hidden" name="row[body_photos][back]" id="backPhotoValue">
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+
+                            <div class="layui-form-item">
+                                <div class="layui-input-block">
+                                    <blockquote class="layui-elem-quote layui-quote-nm">
+                                        <p>提示:身体照片用于AI分析,建议穿着贴身衣物拍摄,确保身体轮廓清晰可见。</p>
+                                    </blockquote>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="layui-form-item" style="margin-top: 30px; text-align: center;">
+                <button class="layui-btn" lay-submit lay-filter="submit-btn">立即创建</button>
+                <button type="reset" class="layui-btn layui-btn-primary">重置</button>
+            </div>
+        </form>
+    </div>
+
+    <script src="/assets/js/layui.js"></script>
+    <script>
+    layui.use(['form', 'upload', 'layer'], function(){
+        var form = layui.form;
+        var upload = layui.upload;
+        var layer = layui.layer;
+
+        // 档案照片上传
+        upload.render({
+            elem: '#uploadProfilePhoto',
+            url: '{:url("common/upload/api")}',
+            accept: 'images',
+            size: 2048,
+            done: function(res){
+                if(res.code == 1){
+                    $('#profilePhotoPreview').attr('src', res.data.url).show();
+                    $('#profilePhotoValue').val(res.data.url);
+                } else {
+                    layer.msg(res.msg);
+                }
+            }
+        });
+
+        // 正面照上传
+        upload.render({
+            elem: '#uploadFrontPhoto',
+            url: '{:url("common/upload/api")}',
+            accept: 'images',
+            size: 2048,
+            done: function(res){
+                if(res.code == 1){
+                    $('#frontPhotoPreview').attr('src', res.data.url).show();
+                    $('#frontPhotoValue').val(res.data.url);
+                } else {
+                    layer.msg(res.msg);
+                }
+            }
+        });
+
+        // 侧面照上传
+        upload.render({
+            elem: '#uploadSidePhoto',
+            url: '{:url("common/upload/api")}',
+            accept: 'images',
+            size: 2048,
+            done: function(res){
+                if(res.code == 1){
+                    $('#sidePhotoPreview').attr('src', res.data.url).show();
+                    $('#sidePhotoValue').val(res.data.url);
+                } else {
+                    layer.msg(res.msg);
+                }
+            }
+        });
+
+        // 背面照上传
+        upload.render({
+            elem: '#uploadBackPhoto',
+            url: '{:url("common/upload/api")}',
+            accept: 'images',
+            size: 2048,
+            done: function(res){
+                if(res.code == 1){
+                    $('#backPhotoPreview').attr('src', res.data.url).show();
+                    $('#backPhotoValue').val(res.data.url);
+                } else {
+                    layer.msg(res.msg);
+                }
+            }
+        });
+
+        // 监听档案类型变化
+        form.on('radio()', function(data){
+            if(data.elem.name === 'row[is_own]') {
+                var relationInput = $('input[name="row[relation]"]');
+                if(data.value === '1') {
+                    relationInput.val('本人').prop('readonly', true);
+                } else {
+                    relationInput.val('').prop('readonly', false);
+                }
+            }
+        });
+
+        // 初始化关系字段
+        if($('input[name="row[is_own]"]:checked').val() === '1') {
+            $('input[name="row[relation]"]').val('本人').prop('readonly', true);
+        }
+
+        // 监听提交
+        form.on('submit(submit-btn)', function(data){
+            var loading = layer.load(2, {content: '正在创建档案...'});
+            
+            $.post('{:url("add")}', data.field, function(res){
+                layer.close(loading);
+                if(res.code === 1){
+                    layer.msg('档案创建成功!', {icon: 1}, function(){
+                        // 关闭当前页面
+                        var index = parent.layer.getFrameIndex(window.name);
+                        parent.layer.close(index);
+                    });
+                } else {
+                    layer.msg(res.msg, {icon: 2});
+                }
+            });
+            
+            return false;
+        });
+    });
+    </script>
+</body>
+</html>

+ 577 - 0
application/admin/view/body_profile/ai_measurement.html

@@ -0,0 +1,577 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>AI身体测量</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+    <link rel="stylesheet" href="/assets/css/layui.css">
+    <link rel="stylesheet" href="/assets/css/admin.css">
+    <style>
+        .measurement-container {
+            display: flex;
+            gap: 30px;
+            margin-top: 20px;
+        }
+        .figure-container {
+            flex: 1;
+            position: relative;
+            background: #f8f9fa;
+            border-radius: 10px;
+            padding: 20px;
+            text-align: center;
+        }
+        .body-figure {
+            position: relative;
+            width: 300px;
+            height: 600px;
+            margin: 0 auto;
+            background: url('/assets/images/body_figure_male.png') no-repeat center center;
+            background-size: contain;
+        }
+        .body-figure.female {
+            background-image: url('/assets/images/body_figure_female.png');
+        }
+        .measurement-point {
+            position: absolute;
+            background: #1E9FFF;
+            color: white;
+            padding: 4px 8px;
+            border-radius: 15px;
+            font-size: 12px;
+            white-space: nowrap;
+            cursor: pointer;
+            z-index: 10;
+        }
+        .measurement-point::before {
+            content: '';
+            position: absolute;
+            width: 2px;
+            height: 20px;
+            background: #1E9FFF;
+            top: 50%;
+            transform: translateY(-50%);
+        }
+        .measurement-point.left::before {
+            right: 100%;
+        }
+        .measurement-point.right::before {
+            left: 100%;
+        }
+        .measurement-line {
+            position: absolute;
+            height: 1px;
+            background: #1E9FFF;
+            z-index: 5;
+        }
+        .data-container {
+            flex: 1;
+        }
+        .basic-info {
+            background: #fff;
+            border-radius: 8px;
+            padding: 20px;
+            margin-bottom: 20px;
+            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+        }
+        .measurement-table {
+            background: #fff;
+            border-radius: 8px;
+            padding: 20px;
+            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+        }
+        .measurement-table table {
+            width: 100%;
+            border-collapse: collapse;
+        }
+        .measurement-table th,
+        .measurement-table td {
+            padding: 12px;
+            text-align: left;
+            border-bottom: 1px solid #eee;
+        }
+        .measurement-table th {
+            background: #f8f9fa;
+            font-weight: bold;
+        }
+        .measurement-value {
+            font-weight: bold;
+            color: #1E9FFF;
+        }
+        .measurement-value.empty {
+            color: #ccc;
+        }
+        .confidence-badge {
+            display: inline-block;
+            background: #52c41a;
+            color: white;
+            padding: 2px 8px;
+            border-radius: 10px;
+            font-size: 12px;
+            margin-left: 10px;
+        }
+        .confidence-badge.medium {
+            background: #fa8c16;
+        }
+        .confidence-badge.low {
+            background: #f5222d;
+        }
+        .ai-controls {
+            text-align: center;
+            margin: 20px 0;
+        }
+        .loading-overlay {
+            position: fixed;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: 100%;
+            background: rgba(255,255,255,0.8);
+            z-index: 1000;
+            display: none;
+            align-items: center;
+            justify-content: center;
+            flex-direction: column;
+        }
+        .loading-content {
+            text-align: center;
+        }
+        .loading-spinner {
+            width: 60px;
+            height: 60px;
+            border: 4px solid #f3f3f3;
+            border-top: 4px solid #1E9FFF;
+            border-radius: 50%;
+            animation: spin 1s linear infinite;
+            margin-bottom: 20px;
+        }
+        @keyframes spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+        }
+        .progress-bar {
+            width: 300px;
+            height: 6px;
+            background: #f0f0f0;
+            border-radius: 3px;
+            overflow: hidden;
+            margin: 10px auto;
+        }
+        .progress-fill {
+            height: 100%;
+            background: #1E9FFF;
+            border-radius: 3px;
+            transition: width 0.3s ease;
+        }
+        .warning-item {
+            background: #fff7e6;
+            border: 1px solid #ffd591;
+            padding: 8px 12px;
+            border-radius: 4px;
+            margin-bottom: 5px;
+            font-size: 12px;
+            color: #fa8c16;
+        }
+    </style>
+</head>
+<body>
+    <div class="layui-fluid" style="padding: 15px;">
+        <!-- 头部信息 -->
+        <div class="layui-card">
+            <div class="layui-card-header">
+                <h3>AI身体测量 - {$profile.profile_name}</h3>
+                <span class="layui-badge layui-bg-blue">{$profile.gender_text}</span>
+                <span class="layui-badge layui-bg-green">{$profile.is_own_text}</span>
+            </div>
+            <div class="layui-card-body">
+                <!-- AI控制按钮 -->
+                <div class="ai-controls">
+                    <button class="layui-btn layui-btn-lg" id="startAnalysis">
+                        <i class="layui-icon layui-icon-play"></i> 开始AI分析
+                    </button>
+                    <button class="layui-btn layui-btn-normal layui-btn-lg" id="retryAnalysis" style="display: none;">
+                        <i class="layui-icon layui-icon-refresh"></i> 重新分析
+                    </button>
+                    <button class="layui-btn layui-btn-warm layui-btn-lg" id="saveResult" style="display: none;">
+                        <i class="layui-icon layui-icon-ok"></i> 保存结果
+                    </button>
+                </div>
+
+                <!-- 置信度和警告信息 -->
+                <div id="analysisInfo" style="display: none;">
+                    <div style="text-align: center; margin: 10px 0;">
+                        <span>分析置信度:</span>
+                        <span id="confidenceValue">85%</span>
+                        <span id="confidenceBadge" class="confidence-badge">高</span>
+                    </div>
+                    <div id="warningsContainer"></div>
+                </div>
+
+                <!-- 测量结果展示 -->
+                <div class="measurement-container">
+                    <!-- 人体示意图 -->
+                    <div class="figure-container">
+                        <h4>身体示意图</h4>
+                        <div id="bodyFigure" class="body-figure {if $profile.gender == 2}female{/if}">
+                            <!-- 测量点将通过JavaScript动态添加 -->
+                        </div>
+                    </div>
+
+                    <!-- 数据表格 -->
+                    <div class="data-container">
+                        <!-- 基础信息 -->
+                        <div class="basic-info">
+                            <h4>身体数据</h4>
+                            <table>
+                                <tr>
+                                    <td><strong>身高</strong></td>
+                                    <td>{$profile.height|default='--'}cm</td>
+                                    <td><strong>体重</strong></td>
+                                    <td>{$profile.weight|default='--'}kg</td>
+                                </tr>
+                            </table>
+                        </div>
+
+                        <!-- 测量数据表格 -->
+                        <div class="measurement-table">
+                            <h4>测量数据</h4>
+                            <table id="measurementTable">
+                                <thead>
+                                    <tr>
+                                        <th>部位</th>
+                                        <th>数值(cm)</th>
+                                        <th>部位</th>
+                                        <th>数值(cm)</th>
+                                    </tr>
+                                </thead>
+                                <tbody id="measurementTableBody">
+                                    <!-- 数据将通过JavaScript动态填充 -->
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- 加载遮罩 -->
+    <div class="loading-overlay" id="loadingOverlay">
+        <div class="loading-content">
+            <div class="loading-spinner"></div>
+            <div id="loadingText">AI正在分析您的身体照片...</div>
+            <div class="progress-bar">
+                <div class="progress-fill" id="progressFill" style="width: 0%"></div>
+            </div>
+            <div id="progressText">0%</div>
+        </div>
+    </div>
+
+    <script src="/assets/js/layui.js"></script>
+    <script>
+    layui.use(['layer'], function(){
+        var layer = layui.layer;
+        var profileId = '{$profile.id}';
+        var currentTaskId = null;
+        var analysisInterval = null;
+
+        // 初始化页面
+        initializePage();
+
+        // 开始AI分析
+        $('#startAnalysis').click(function() {
+            startAiAnalysis();
+        });
+
+        // 重新分析
+        $('#retryAnalysis').click(function() {
+            retryAnalysis();
+        });
+
+        // 保存结果
+        $('#saveResult').click(function() {
+            saveAnalysisResult();
+        });
+
+        // 初始化页面数据
+        function initializePage() {
+            // 加载默认的空数据表格
+            renderMeasurementTable({});
+            
+            // 检查是否有进行中的任务
+            checkExistingTask();
+        }
+
+        // 检查现有任务
+        function checkExistingTask() {
+            $.get('/api/ai_measurement/getResult', {
+                profile_id: profileId
+            }, function(response) {
+                if (response.code === 1 && response.data) {
+                    switch (response.data.status) {
+                        case 'processing':
+                        case 'pending':
+                            showLoadingOverlay();
+                            startPolling();
+                            break;
+                        case 'completed':
+                            renderAnalysisResult(response.data.data);
+                            break;
+                        case 'failed':
+                            if (response.data.can_retry) {
+                                $('#retryAnalysis').show();
+                            }
+                            break;
+                    }
+                }
+            });
+        }
+
+        // 开始AI分析
+        function startAiAnalysis() {
+            // 获取档案的身体照片
+            $.get('/api/body_profile/detail', {
+                profile_id: profileId
+            }, function(response) {
+                if (response.code === 1) {
+                    var photos = response.data.body_photos_array;
+                    
+                    if (!photos.front || !photos.side || !photos.back) {
+                        layer.msg('请先上传完整的身体照片(正面、侧面、背面)');
+                        return;
+                    }
+
+                    // 发起AI分析请求
+                    $.post('/api/ai_measurement/startAnalysis', {
+                        profile_id: profileId,
+                        photos: photos
+                    }, function(res) {
+                        if (res.code === 1) {
+                            currentTaskId = res.data.task_id;
+                            showLoadingOverlay();
+                            startPolling();
+                        } else {
+                            layer.msg(res.msg);
+                        }
+                    });
+                } else {
+                    layer.msg(response.msg);
+                }
+            });
+        }
+
+        // 显示加载遮罩
+        function showLoadingOverlay() {
+            $('#loadingOverlay').show();
+            $('#startAnalysis').hide();
+        }
+
+        // 隐藏加载遮罩
+        function hideLoadingOverlay() {
+            $('#loadingOverlay').hide();
+            $('#startAnalysis').show();
+        }
+
+        // 开始轮询检查结果
+        function startPolling() {
+            if (analysisInterval) {
+                clearInterval(analysisInterval);
+            }
+
+            analysisInterval = setInterval(function() {
+                checkAnalysisResult();
+            }, 2000); // 每2秒检查一次
+        }
+
+        // 检查分析结果
+        function checkAnalysisResult() {
+            var params = currentTaskId ? {task_id: currentTaskId} : {profile_id: profileId};
+            
+            $.get('/api/ai_measurement/getResult', params, function(response) {
+                if (response.code === 1) {
+                    switch (response.data.status) {
+                        case 'pending':
+                            updateProgress(10, '任务排队中...');
+                            break;
+                            
+                        case 'processing':
+                            var progress = response.data.progress || 50;
+                            updateProgress(progress, 'AI正在分析您的身体照片...');
+                            break;
+                            
+                        case 'completed':
+                            clearInterval(analysisInterval);
+                            hideLoadingOverlay();
+                            renderAnalysisResult(response.data.data);
+                            break;
+                            
+                        case 'failed':
+                            clearInterval(analysisInterval);
+                            hideLoadingOverlay();
+                            layer.msg('分析失败: ' + response.data.message);
+                            if (response.data.can_retry) {
+                                $('#retryAnalysis').show();
+                            }
+                            break;
+                    }
+                }
+            });
+        }
+
+        // 更新进度
+        function updateProgress(progress, text) {
+            $('#progressFill').css('width', progress + '%');
+            $('#progressText').text(progress + '%');
+            $('#loadingText').text(text);
+        }
+
+        // 渲染分析结果
+        function renderAnalysisResult(data) {
+            // 显示置信度
+            if (data.confidence) {
+                var confidence = Math.round(data.confidence * 100);
+                $('#confidenceValue').text(confidence + '%');
+                
+                var badge = $('#confidenceBadge');
+                if (confidence >= 80) {
+                    badge.removeClass('medium low').addClass('high').text('高');
+                } else if (confidence >= 60) {
+                    badge.removeClass('high low').addClass('medium').text('中');
+                } else {
+                    badge.removeClass('high medium').addClass('low').text('低');
+                }
+                
+                $('#analysisInfo').show();
+            }
+
+            // 显示警告信息
+            if (data.warnings && data.warnings.length > 0) {
+                var warningsHtml = '';
+                data.warnings.forEach(function(warning) {
+                    warningsHtml += '<div class="warning-item">' + warning + '</div>';
+                });
+                $('#warningsContainer').html(warningsHtml);
+            }
+
+            // 渲染测量点
+            renderMeasurementPoints(data.measurements);
+            
+            // 渲染数据表格
+            renderMeasurementTable(data.measurements);
+
+            // 显示保存按钮
+            $('#saveResult').show();
+        }
+
+        // 渲染测量点
+        function renderMeasurementPoints(measurements) {
+            var figure = $('#bodyFigure');
+            figure.find('.measurement-point, .measurement-line').remove();
+
+            if (!measurements) return;
+
+            Object.keys(measurements).forEach(function(field) {
+                var measurement = measurements[field];
+                if (!measurement.value || !measurement.position) return;
+
+                var point = $('<div class="measurement-point ' + measurement.side + '">')
+                    .text(measurement.label + ': ' + measurement.value + measurement.unit)
+                    .css({
+                        left: (measurement.position.x * 100) + '%',
+                        top: (measurement.position.y * 100) + '%'
+                    });
+
+                figure.append(point);
+            });
+        }
+
+        // 渲染测量数据表格
+        function renderMeasurementTable(measurements) {
+            var tbody = $('#measurementTableBody');
+            tbody.empty();
+
+            // 默认字段配置
+            var defaultFields = [
+                ['胸围', 'chest'], ['腰围', 'waist'],
+                ['臀围', 'hip'], ['大腿围', 'thigh'],
+                ['小腿围', 'calf'], ['上臂围', 'upper_arm'],
+                ['肩宽', 'shoulder_width'], ['颈围', 'neck']
+            ];
+
+            // 按两列排列
+            for (var i = 0; i < defaultFields.length; i += 2) {
+                var row = '<tr>';
+                
+                for (var j = 0; j < 2; j++) {
+                    if (i + j < defaultFields.length) {
+                        var field = defaultFields[i + j];
+                        var label = field[0];
+                        var key = field[1];
+                        var value = measurements[key] ? measurements[key].value : null;
+                        var displayValue = value ? value + 'cm' : '--';
+                        var cssClass = value ? 'measurement-value' : 'measurement-value empty';
+                        
+                        row += '<td>' + label + '</td>';
+                        row += '<td><span class="' + cssClass + '">' + displayValue + '</span></td>';
+                    } else {
+                        row += '<td></td><td></td>';
+                    }
+                }
+                
+                row += '</tr>';
+                tbody.append(row);
+            }
+        }
+
+        // 重新分析
+        function retryAnalysis() {
+            if (!currentTaskId) {
+                layer.msg('没有可重试的任务');
+                return;
+            }
+
+            $.post('/api/ai_measurement/retryAnalysis', {
+                task_id: currentTaskId
+            }, function(response) {
+                if (response.code === 1) {
+                    $('#retryAnalysis').hide();
+                    showLoadingOverlay();
+                    startPolling();
+                } else {
+                    layer.msg(response.msg);
+                }
+            });
+        }
+
+        // 保存分析结果
+        function saveAnalysisResult() {
+            if (!currentTaskId) {
+                layer.msg('没有可保存的结果');
+                return;
+            }
+
+            layer.confirm('确定要保存这次AI测量结果吗?', {
+                icon: 3,
+                title: '确认保存'
+            }, function(index) {
+                $.post('/api/ai_measurement/saveResult', {
+                    task_id: currentTaskId
+                }, function(response) {
+                    if (response.code === 1) {
+                        layer.msg('测量结果已保存', {icon: 1});
+                        $('#saveResult').hide();
+                        
+                        // 可以跳转到测量历史页面
+                        setTimeout(function() {
+                            parent.layer.close(parent.layer.getFrameIndex(window.name));
+                        }, 1500);
+                    } else {
+                        layer.msg(response.msg);
+                    }
+                });
+                layer.close(index);
+            });
+        }
+    });
+    </script>
+</body>
+</html>

+ 345 - 0
application/admin/view/body_profile/body_types.html

@@ -0,0 +1,345 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>体型选择管理</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+    <link rel="stylesheet" href="/assets/css/layui.css">
+    <link rel="stylesheet" href="/assets/css/admin.css">
+    <style>
+        .body-type-category {
+            margin-bottom: 30px;
+            border: 1px solid #e6e6e6;
+            border-radius: 6px;
+            overflow: hidden;
+        }
+        .category-header {
+            background: #f8f8f8;
+            padding: 15px 20px;
+            border-bottom: 1px solid #e6e6e6;
+            font-weight: bold;
+            color: #333;
+        }
+        .category-content {
+            padding: 20px;
+        }
+        .type-options {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 15px;
+        }
+        .type-option {
+            border: 2px solid #e6e6e6;
+            border-radius: 8px;
+            padding: 15px;
+            text-align: center;
+            cursor: pointer;
+            transition: all 0.3s;
+            min-width: 120px;
+            background: #fff;
+        }
+        .type-option:hover {
+            border-color: #1E9FFF;
+            box-shadow: 0 2px 8px rgba(30, 159, 255, 0.2);
+        }
+        .type-option.selected {
+            border-color: #1E9FFF;
+            background: #f0f9ff;
+        }
+        .type-option img {
+            width: 80px;
+            height: 100px;
+            object-fit: cover;
+            margin-bottom: 10px;
+            border-radius: 4px;
+        }
+        .type-option .type-name {
+            font-weight: bold;
+            color: #333;
+            margin-bottom: 5px;
+        }
+        .type-option .type-description {
+            font-size: 12px;
+            color: #666;
+            line-height: 1.4;
+        }
+        .type-option.selected .type-name {
+            color: #1E9FFF;
+        }
+        .placeholder-img {
+            width: 80px;
+            height: 100px;
+            background: #f5f5f5;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            margin-bottom: 10px;
+            border-radius: 4px;
+            color: #999;
+            font-size: 12px;
+        }
+        .ai-recommendation {
+            background: #fff7e6;
+            border: 1px solid #ffd591;
+            padding: 10px;
+            border-radius: 4px;
+            margin-bottom: 15px;
+        }
+        .ai-recommendation .title {
+            color: #fa8c16;
+            font-weight: bold;
+            margin-bottom: 5px;
+        }
+        .ai-recommendation .content {
+            color: #666;
+            font-size: 14px;
+        }
+        .confidence-badge {
+            display: inline-block;
+            background: #52c41a;
+            color: white;
+            padding: 2px 6px;
+            border-radius: 10px;
+            font-size: 11px;
+            margin-left: 5px;
+        }
+    </style>
+</head>
+<body>
+    <div class="layui-fluid" style="padding: 15px;">
+        <div class="layui-card">
+            <div class="layui-card-header">
+                <h3>{$profile.profile_name} - 体型选择</h3>
+                <div style="float: right;">
+                    <span class="layui-badge layui-bg-blue">{$profile.gender_text}</span>
+                    <span class="layui-badge layui-bg-green">{$profile.is_own_text}</span>
+                </div>
+            </div>
+            <div class="layui-card-body">
+                <!-- AI推荐提示 -->
+                <div class="ai-recommendation" id="aiRecommendation" style="display: none;">
+                    <div class="title">
+                        <i class="layui-icon layui-icon-tips"></i> AI智能推荐
+                    </div>
+                    <div class="content" id="recommendationContent">
+                        基于您的身体测量数据,AI为您推荐了最适合的体型选择。
+                    </div>
+                </div>
+
+                <form class="layui-form" action="{:url('bodyTypes')}" method="post" lay-filter="body-type-form">
+                    <input type="hidden" name="profile_id" value="{$profile.id}">
+                    
+                    <!-- 体型分类选择 -->
+                    <div id="bodyTypeCategories">
+                        {foreach $bodyTypeCategories as $categoryKey => $category}
+                        <div class="body-type-category">
+                            <div class="category-header">
+                                <i class="{$category.icon|default='layui-icon layui-icon-template-1'}"></i>
+                                {$category.name}
+                                <span style="font-weight: normal; color: #666; margin-left: 10px;">
+                                    {$category.description}
+                                </span>
+                            </div>
+                            <div class="category-content">
+                                <div class="type-options" data-category="{$categoryKey}">
+                                    {foreach $category.types as $type}
+                                    <div class="type-option {if isset($userSelections[$categoryKey]) && $userSelections[$categoryKey]['type_id'] == $type['id']}selected{/if}"
+                                         data-type-id="{$type.id}" 
+                                         data-category="{$categoryKey}">
+                                        {if $type.type_image}
+                                            <img src="{$type.full_image_url}" alt="{$type.type_name}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
+                                            <div class="placeholder-img" style="display: none;">暂无图片</div>
+                                        {else}
+                                            <div class="placeholder-img">暂无图片</div>
+                                        {/if}
+                                        <div class="type-name">{$type.type_name}</div>
+                                        <div class="type-description">{$type.description}</div>
+                                    </div>
+                                    {/foreach}
+                                </div>
+                                <input type="hidden" name="selections[{$categoryKey}]" value="{if isset($userSelections[$categoryKey])}{$userSelections[$categoryKey]['type_id']}{/if}">
+                            </div>
+                        </div>
+                        {/foreach}
+                    </div>
+
+                    <div class="layui-form-item" style="margin-top: 30px; text-align: center;">
+                        <button class="layui-btn" lay-submit lay-filter="submit-btn">
+                            <i class="layui-icon layui-icon-ok"></i> 保存选择
+                        </button>
+                        <button type="button" class="layui-btn layui-btn-primary" onclick="getAIRecommendation()">
+                            <i class="layui-icon layui-icon-light"></i> 获取AI推荐
+                        </button>
+                        <button type="button" class="layui-btn layui-btn-normal" onclick="resetSelections()">
+                            <i class="layui-icon layui-icon-refresh"></i> 重置选择
+                        </button>
+                    </div>
+                </form>
+            </div>
+        </div>
+    </div>
+
+    <script src="/assets/js/layui.js"></script>
+    <script>
+    layui.use(['form', 'layer'], function(){
+        var form = layui.form;
+        var layer = layui.layer;
+
+        // 体型选择事件
+        $('.type-option').click(function(){
+            var $this = $(this);
+            var category = $this.data('category');
+            var typeId = $this.data('type-id');
+            
+            // 取消同分类下其他选项的选中状态
+            $this.siblings('.type-option').removeClass('selected');
+            
+            // 切换当前选项的选中状态
+            $this.toggleClass('selected');
+            
+            // 更新隐藏字段值
+            var $hiddenInput = $('input[name="selections[' + category + ']"]');
+            if ($this.hasClass('selected')) {
+                $hiddenInput.val(typeId);
+            } else {
+                $hiddenInput.val('');
+            }
+        });
+
+        // 监听提交
+        form.on('submit(submit-btn)', function(data){
+            var loading = layer.load(2, {content: '正在保存...'});
+            
+            $.post('{:url("bodyTypes")}', data.field, function(res){
+                layer.close(loading);
+                if(res.code === 1){
+                    layer.msg('保存成功!', {icon: 1});
+                } else {
+                    layer.msg(res.msg, {icon: 2});
+                }
+            });
+            
+            return false;
+        });
+
+        // 页面加载时获取AI推荐
+        getAIRecommendation();
+    });
+
+    // 获取AI推荐
+    function getAIRecommendation() {
+        var profileId = '{$profile.id}';
+        
+        $.get('/api/body_profile/getBodyTypeRecommendation', {profile_id: profileId}, function(res) {
+            if (res.code === 1 && res.data && Object.keys(res.data).length > 0) {
+                showAIRecommendation(res.data);
+            } else {
+                $('#aiRecommendation').hide();
+            }
+        });
+    }
+
+    // 显示AI推荐
+    function showAIRecommendation(recommendations) {
+        var content = '';
+        var hasRecommendations = false;
+
+        for (var category in recommendations) {
+            var rec = recommendations[category];
+            content += '<div style="margin-bottom: 5px;">';
+            content += '<strong>' + getCategoryName(category) + ':</strong> ';
+            content += rec.type_name;
+            content += '<span class="confidence-badge">置信度: ' + Math.round(rec.confidence * 100) + '%</span>';
+            content += '</div>';
+            
+            // 高亮推荐选项
+            highlightRecommendedType(category, rec.type_name);
+            hasRecommendations = true;
+        }
+
+        if (hasRecommendations) {
+            $('#recommendationContent').html(content);
+            $('#aiRecommendation').show();
+        }
+    }
+
+    // 高亮推荐的体型选项
+    function highlightRecommendedType(category, typeName) {
+        $('.type-options[data-category="' + category + '"] .type-option').each(function() {
+            var $option = $(this);
+            if ($option.find('.type-name').text() === typeName) {
+                $option.css({
+                    'border-color': '#52c41a',
+                    'box-shadow': '0 0 0 2px rgba(82, 196, 26, 0.2)'
+                });
+                
+                // 添加推荐标识
+                if (!$option.find('.recommend-badge').length) {
+                    $option.find('.type-name').append('<span class="recommend-badge" style="background: #52c41a; color: white; padding: 1px 4px; border-radius: 8px; font-size: 10px; margin-left: 5px;">推荐</span>');
+                }
+            }
+        });
+    }
+
+    // 获取分类中文名称
+    function getCategoryName(category) {
+        var names = {
+            'shoulder': '肩型',
+            'chest': '胸型',
+            'bust': '胸型',
+            'waist': '腰型',
+            'hip': '臀型',
+            'leg': '腿型'
+        };
+        return names[category] || category;
+    }
+
+    // 重置选择
+    function resetSelections() {
+        layui.layer.confirm('确定要重置所有选择吗?', {
+            icon: 3,
+            title: '确认重置'
+        }, function(index) {
+            $('.type-option').removeClass('selected');
+            $('input[name^="selections"]').val('');
+            $('.recommend-badge').remove();
+            $('.type-option').css({
+                'border-color': '#e6e6e6',
+                'box-shadow': 'none'
+            });
+            layui.layer.close(index);
+            layui.layer.msg('已重置选择');
+        });
+    }
+
+    // 应用AI推荐
+    function applyAIRecommendation() {
+        var profileId = '{$profile.id}';
+        
+        $.get('/api/body_profile/getBodyTypeRecommendation', {profile_id: profileId}, function(res) {
+            if (res.code === 1 && res.data) {
+                for (var category in res.data) {
+                    var rec = res.data[category];
+                    selectTypeByName(category, rec.type_name);
+                }
+                layui.layer.msg('已应用AI推荐');
+            } else {
+                layui.layer.msg('暂无AI推荐数据,请先完善身体测量数据');
+            }
+        });
+    }
+
+    // 根据名称选择体型
+    function selectTypeByName(category, typeName) {
+        $('.type-options[data-category="' + category + '"] .type-option').each(function() {
+            var $option = $(this);
+            if ($option.find('.type-name').text() === typeName) {
+                $option.siblings('.type-option').removeClass('selected');
+                $option.addClass('selected');
+                $('input[name="selections[' + category + ']"]').val($option.data('type-id'));
+            }
+        });
+    }
+    </script>
+</body>
+</html>

+ 506 - 0
application/admin/view/body_profile/detail.html

@@ -0,0 +1,506 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>档案详情</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+    <link rel="stylesheet" href="/assets/css/layui.css">
+    <link rel="stylesheet" href="/assets/css/admin.css">
+    <style>
+        .profile-header {
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+            padding: 30px;
+            border-radius: 8px;
+            margin-bottom: 20px;
+        }
+        .profile-avatar {
+            width: 100px;
+            height: 100px;
+            border-radius: 50%;
+            border: 4px solid rgba(255,255,255,0.3);
+            object-fit: cover;
+            float: left;
+            margin-right: 20px;
+        }
+        .profile-info {
+            overflow: hidden;
+        }
+        .profile-name {
+            font-size: 24px;
+            font-weight: bold;
+            margin-bottom: 10px;
+        }
+        .profile-meta {
+            font-size: 14px;
+            opacity: 0.9;
+        }
+        .profile-meta span {
+            margin-right: 20px;
+        }
+        .info-card {
+            margin-bottom: 20px;
+        }
+        .info-grid {
+            display: grid;
+            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+            gap: 15px;
+        }
+        .info-item {
+            background: #f8f9fa;
+            padding: 15px;
+            border-radius: 6px;
+            border-left: 4px solid #1E9FFF;
+        }
+        .info-item .label {
+            font-size: 12px;
+            color: #666;
+            margin-bottom: 5px;
+        }
+        .info-item .value {
+            font-size: 16px;
+            font-weight: bold;
+            color: #333;
+        }
+        .body-photos {
+            display: flex;
+            gap: 15px;
+            margin-top: 15px;
+        }
+        .photo-item {
+            text-align: center;
+            flex: 1;
+        }
+        .photo-item img {
+            width: 120px;
+            height: 160px;
+            object-fit: cover;
+            border-radius: 6px;
+            border: 2px solid #e6e6e6;
+        }
+        .photo-item .photo-label {
+            margin-top: 8px;
+            font-size: 14px;
+            color: #666;
+        }
+        .no-photo {
+            width: 120px;
+            height: 160px;
+            background: #f5f5f5;
+            border: 2px dashed #ccc;
+            border-radius: 6px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            color: #999;
+            font-size: 12px;
+        }
+        .body-type-display {
+            display: flex;
+            flex-wrap: wrap;
+            gap: 15px;
+            margin-top: 15px;
+        }
+        .body-type-item {
+            background: white;
+            border: 1px solid #e6e6e6;
+            border-radius: 6px;
+            padding: 15px;
+            text-align: center;
+            min-width: 120px;
+        }
+        .body-type-item img {
+            width: 60px;
+            height: 80px;
+            object-fit: cover;
+            margin-bottom: 8px;
+        }
+        .body-type-item .type-category {
+            font-size: 12px;
+            color: #666;
+            margin-bottom: 5px;
+        }
+        .body-type-item .type-name {
+            font-weight: bold;
+            color: #333;
+        }
+        .measurements-table {
+            margin-top: 15px;
+        }
+        .measurements-table .layui-table td {
+            padding: 8px 15px;
+        }
+        .bmi-indicator {
+            display: inline-block;
+            padding: 4px 12px;
+            border-radius: 20px;
+            color: white;
+            font-size: 12px;
+            font-weight: bold;
+        }
+        .bmi-normal { background: #52c41a; }
+        .bmi-thin { background: #13c2c2; }
+        .bmi-overweight { background: #fa8c16; }
+        .bmi-obese { background: #f5222d; }
+        .health-score {
+            text-align: center;
+            padding: 20px;
+        }
+        .health-score .score {
+            font-size: 48px;
+            font-weight: bold;
+            color: #1E9FFF;
+            margin-bottom: 10px;
+        }
+        .health-score .level {
+            font-size: 18px;
+            color: #666;
+        }
+    </style>
+</head>
+<body>
+    <div class="layui-fluid" style="padding: 15px;">
+        <!-- 档案头部 -->
+        <div class="profile-header">
+            {if $profile.profile_photo}
+            <img src="{$profile.profile_photo}" alt="档案照片" class="profile-avatar">
+            {else}
+            <div class="profile-avatar" style="background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 36px;">
+                <i class="layui-icon layui-icon-username"></i>
+            </div>
+            {/if}
+            <div class="profile-info">
+                <div class="profile-name">{$profile.profile_name}</div>
+                <div class="profile-meta">
+                    <span><i class="layui-icon layui-icon-username"></i> {$profile.user.username|default='未知用户'}</span>
+                    <span><i class="layui-icon layui-icon-group"></i> {$profile.relation|default='未设置'}</span>
+                    <span><i class="layui-icon layui-icon-date"></i> 创建于 {$profile.createtime|date='Y-m-d H:i'}</span>
+                </div>
+            </div>
+        </div>
+
+        <div class="layui-row layui-col-space15">
+            <!-- 基础信息 -->
+            <div class="layui-col-md6">
+                <div class="layui-card info-card">
+                    <div class="layui-card-header">
+                        <i class="layui-icon layui-icon-user"></i> 基础信息
+                    </div>
+                    <div class="layui-card-body">
+                        <div class="info-grid">
+                            <div class="info-item">
+                                <div class="label">性别</div>
+                                <div class="value">{$profile.gender_text}</div>
+                            </div>
+                            <div class="info-item">
+                                <div class="label">年龄</div>
+                                <div class="value">{$profile.age|default='未设置'} {if $profile.age}岁{/if}</div>
+                            </div>
+                            <div class="info-item">
+                                <div class="label">身高</div>
+                                <div class="value">{$profile.height|default='未测量'} {if $profile.height}cm{/if}</div>
+                            </div>
+                            <div class="info-item">
+                                <div class="label">体重</div>
+                                <div class="value">{$profile.weight|default='未测量'} {if $profile.weight}kg{/if}</div>
+                            </div>
+                            <div class="info-item">
+                                <div class="label">BMI指数</div>
+                                <div class="value">
+                                    {if $profile.height && $profile.weight}
+                                        {php}
+                                        $bmi = round($profile['weight'] / pow($profile['height']/100, 2), 1);
+                                        $bmiLevel = '';
+                                        $bmiClass = '';
+                                        if ($bmi < 18.5) {
+                                            $bmiLevel = '偏瘦';
+                                            $bmiClass = 'bmi-thin';
+                                        } elseif ($bmi < 24) {
+                                            $bmiLevel = '正常';
+                                            $bmiClass = 'bmi-normal';
+                                        } elseif ($bmi < 28) {
+                                            $bmiLevel = '超重';
+                                            $bmiClass = 'bmi-overweight';
+                                        } else {
+                                            $bmiLevel = '肥胖';
+                                            $bmiClass = 'bmi-obese';
+                                        }
+                                        echo $bmi;
+                                        {/php}
+                                        <span class="bmi-indicator {$bmiClass}">{$bmiLevel}</span>
+                                    {else}
+                                        未计算
+                                    {/if}
+                                </div>
+                            </div>
+                            <div class="info-item">
+                                <div class="label">档案类型</div>
+                                <div class="value">{$profile.is_own_text}</div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 健康评分 -->
+            <div class="layui-col-md6">
+                <div class="layui-card info-card">
+                    <div class="layui-card-header">
+                        <i class="layui-icon layui-icon-chart"></i> 健康评估
+                    </div>
+                    <div class="layui-card-body">
+                        {if $profile.ai_report}
+                        <div class="health-score">
+                            <div class="score">{$profile.ai_report.health_score}</div>
+                            <div class="level">{$profile.ai_report.health_level_text}</div>
+                            <div style="margin-top: 15px; font-size: 14px; color: #666;">
+                                身体年龄: {$profile.ai_report.body_age}岁
+                            </div>
+                            <div style="margin-top: 10px; font-size: 12px; color: #999;">
+                                更新时间: {$profile.ai_report.generated_time_text}
+                            </div>
+                        </div>
+                        {else}
+                        <div style="text-align: center; padding: 40px; color: #999;">
+                            <i class="layui-icon layui-icon-file" style="font-size: 48px; margin-bottom: 15px;"></i>
+                            <div>暂无AI分析报告</div>
+                            <div style="margin-top: 10px;">
+                                <button class="layui-btn layui-btn-sm" onclick="generateReport()">
+                                    <i class="layui-icon layui-icon-add-1"></i> 生成报告
+                                </button>
+                            </div>
+                        </div>
+                        {/if}
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 身体照片 -->
+        <div class="layui-card info-card">
+            <div class="layui-card-header">
+                <i class="layui-icon layui-icon-picture"></i> 身体照片
+            </div>
+            <div class="layui-card-body">
+                <div class="body-photos">
+                    <div class="photo-item">
+                        {if $profile.body_photos_array.front}
+                        <img src="{$profile.body_photos_array.front}" alt="正面照">
+                        {else}
+                        <div class="no-photo">暂无正面照</div>
+                        {/if}
+                        <div class="photo-label">正面照</div>
+                    </div>
+                    <div class="photo-item">
+                        {if $profile.body_photos_array.side}
+                        <img src="{$profile.body_photos_array.side}" alt="侧面照">
+                        {else}
+                        <div class="no-photo">暂无侧面照</div>
+                        {/if}
+                        <div class="photo-label">侧面照</div>
+                    </div>
+                    <div class="photo-item">
+                        {if $profile.body_photos_array.back}
+                        <img src="{$profile.body_photos_array.back}" alt="背面照">
+                        {else}
+                        <div class="no-photo">暂无背面照</div>
+                        {/if}
+                        <div class="photo-label">背面照</div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 最新测量数据 -->
+        {if $profile.measurements}
+        <div class="layui-card info-card">
+            <div class="layui-card-header">
+                <i class="layui-icon layui-icon-form"></i> 最新测量数据
+                <span style="float: right; font-size: 12px; color: #666;">
+                    测量日期: {$profile.measurements.measurement_date_text}
+                </span>
+            </div>
+            <div class="layui-card-body">
+                <table class="layui-table measurements-table">
+                    <thead>
+                        <tr>
+                            <th>测量部位</th>
+                            <th>数值(cm)</th>
+                            <th>测量部位</th>
+                            <th>数值(cm)</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        {php}
+                        $measurements = $profile['measurements'];
+                        $fields = [
+                            'chest' => '胸围',
+                            'waist' => '腰围',
+                            'hip' => '臀围',
+                            'thigh' => '大腿围',
+                            'calf' => '小腿围',
+                            'upper_arm' => '上臂围',
+                            'shoulder_width' => '肩宽',
+                            'neck' => '颈围'
+                        ];
+                        if ($profile['gender'] == 2) {
+                            $fields['bust'] = '胸围/乳围';
+                            $fields['underbust'] = '下胸围';
+                        }
+                        $fieldsArray = array_chunk($fields, 2, true);
+                        {/php}
+                        {foreach $fieldsArray as $rowFields}
+                        <tr>
+                            {foreach $rowFields as $field => $label}
+                            <td>{$label}</td>
+                            <td>{$measurements[$field]|default='-'}</td>
+                            {/foreach}
+                            {if count($rowFields) == 1}
+                            <td></td>
+                            <td></td>
+                            {/if}
+                        </tr>
+                        {/foreach}
+                    </tbody>
+                </table>
+            </div>
+        </div>
+        {/if}
+
+        <!-- 体型选择 -->
+        {if $profile.body_types}
+        <div class="layui-card info-card">
+            <div class="layui-card-header">
+                <i class="layui-icon layui-icon-template-1"></i> 体型选择
+            </div>
+            <div class="layui-card-body">
+                <div class="body-type-display">
+                    {foreach $profile.body_types as $category => $bodyType}
+                    <div class="body-type-item">
+                        {if $bodyType.type_image}
+                        <img src="{$bodyType.type_image}" alt="{$bodyType.type_name}">
+                        {else}
+                        <div style="width: 60px; height: 80px; background: #f5f5f5; margin-bottom: 8px; border-radius: 4px;"></div>
+                        {/if}
+                        <div class="type-category">
+                            {php}
+                            $categoryNames = [
+                                'shoulder' => '肩型',
+                                'chest' => '胸型',
+                                'bust' => '胸型',
+                                'waist' => '腰型',
+                                'hip' => '臀型',
+                                'leg' => '腿型'
+                            ];
+                            echo $categoryNames[$category] ?? $category;
+                            {/php}
+                        </div>
+                        <div class="type-name">{$bodyType.type_name}</div>
+                    </div>
+                    {/foreach}
+                </div>
+            </div>
+        </div>
+        {/if}
+
+        <!-- 操作按钮 -->
+        <div style="text-align: center; margin-top: 30px;">
+            <button class="layui-btn" onclick="editProfile()">
+                <i class="layui-icon layui-icon-edit"></i> 编辑档案
+            </button>
+            <button class="layui-btn layui-btn-normal" onclick="manageMeasurements()">
+                <i class="layui-icon layui-icon-form"></i> 测量数据
+            </button>
+            <button class="layui-btn layui-btn-cyan" onclick="aiMeasurement()">
+                <i class="layui-icon layui-icon-light"></i> AI测量
+            </button>
+            <button class="layui-btn layui-btn-warm" onclick="manageBodyTypes()">
+                <i class="layui-icon layui-icon-template-1"></i> 体型选择
+            </button>
+            {if $profile.ai_report}
+            <button class="layui-btn layui-btn-hot" onclick="viewReport()">
+                <i class="layui-icon layui-icon-file"></i> 查看报告
+            </button>
+            {else}
+            <button class="layui-btn layui-btn-hot" onclick="generateReport()">
+                <i class="layui-icon layui-icon-add-1"></i> 生成报告
+            </button>
+            {/if}
+        </div>
+    </div>
+
+    <script src="/assets/js/layui.js"></script>
+    <script>
+    layui.use(['layer'], function(){
+        var layer = layui.layer;
+        
+        window.editProfile = function() {
+            parent.layer.open({
+                type: 2,
+                title: '编辑档案',
+                area: ['800px', '600px'],
+                content: '{:url("edit")}?ids={$profile.id}',
+                end: function() {
+                    location.reload();
+                }
+            });
+        };
+
+        window.manageMeasurements = function() {
+            parent.layer.open({
+                type: 2,
+                title: '测量数据管理',
+                area: ['1000px', '700px'],
+                content: '{:url("measurements")}?profile_id={$profile.id}'
+            });
+        };
+
+        window.aiMeasurement = function() {
+            parent.layer.open({
+                type: 2,
+                title: 'AI身体测量',
+                area: ['1200px', '800px'],
+                content: '{:url("aiMeasurement")}?profile_id={$profile.id}'
+            });
+        };
+
+        window.manageBodyTypes = function() {
+            parent.layer.open({
+                type: 2,
+                title: '体型选择管理',
+                area: ['900px', '600px'],
+                content: '{:url("bodyTypes")}?profile_id={$profile.id}'
+            });
+        };
+
+        window.generateReport = function() {
+            layer.confirm('确定要生成AI分析报告吗?', {
+                icon: 3,
+                title: '提示'
+            }, function(index) {
+                var loading = layer.load(2, {content: '正在生成报告...'});
+                $.post('{:url("generateReport")}', {profile_id: '{$profile.id}'}, function(res) {
+                    layer.close(loading);
+                    if (res.code === 1) {
+                        layer.msg('报告生成成功');
+                        location.reload();
+                    } else {
+                        layer.msg(res.msg);
+                    }
+                });
+                layer.close(index);
+            });
+        };
+
+        window.viewReport = function() {
+            parent.layer.open({
+                type: 2,
+                title: 'AI分析报告',
+                area: ['1000px', '700px'],
+                content: '{:url("viewReport")}?report_id={$profile.ai_report.id|default=0}'
+            });
+        };
+    });
+    </script>
+</body>
+</html>

+ 367 - 0
application/admin/view/body_profile/index.html

@@ -0,0 +1,367 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>身体档案管理</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+    <link rel="stylesheet" href="/assets/css/layui.css">
+    <link rel="stylesheet" href="/assets/css/admin.css">
+</head>
+<body>
+    <div class="layui-fluid">
+        <div class="layui-card">
+            <div class="layui-card-header">
+                <h3>身体档案管理</h3>
+            </div>
+            <div class="layui-card-body">
+                <!-- 搜索条件 -->
+                <div class="layui-form layui-form-pane" lay-filter="search-form">
+                    <div class="layui-form-item">
+                        <div class="layui-inline">
+                            <label class="layui-form-label">关键词</label>
+                            <div class="layui-input-inline">
+                                <input type="text" name="search" placeholder="档案名称/关系" autocomplete="off" class="layui-input">
+                            </div>
+                        </div>
+                        <div class="layui-inline">
+                            <label class="layui-form-label">性别</label>
+                            <div class="layui-input-inline">
+                                <select name="gender">
+                                    <option value="">全部</option>
+                                    <option value="1">男</option>
+                                    <option value="2">女</option>
+                                </select>
+                            </div>
+                        </div>
+                        <div class="layui-inline">
+                            <label class="layui-form-label">档案类型</label>
+                            <div class="layui-input-inline">
+                                <select name="is_own">
+                                    <option value="">全部</option>
+                                    <option value="1">本人档案</option>
+                                    <option value="0">他人档案</option>
+                                </select>
+                            </div>
+                        </div>
+                        <div class="layui-inline">
+                            <button class="layui-btn" lay-submit lay-filter="search-btn">
+                                <i class="layui-icon layui-icon-search"></i> 搜索
+                            </button>
+                            <button type="reset" class="layui-btn layui-btn-primary">重置</button>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 工具栏 -->
+                <div class="layui-row layui-col-space15">
+                    <div class="layui-col-md12">
+                        <div class="layui-btn-group">
+                            <button class="layui-btn" onclick="addProfile()">
+                                <i class="layui-icon layui-icon-add-1"></i> 新增档案
+                            </button>
+                            <button class="layui-btn layui-btn-danger" onclick="batchDelete()">
+                                <i class="layui-icon layui-icon-delete"></i> 批量删除
+                            </button>
+                            <button class="layui-btn layui-btn-normal" onclick="exportData()">
+                                <i class="layui-icon layui-icon-export"></i> 导出数据
+                            </button>
+                            <button class="layui-btn layui-btn-warm" onclick="showStatistics()">
+                                <i class="layui-icon layui-icon-chart"></i> 统计分析
+                            </button>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- 数据表格 -->
+                <table class="layui-hide" id="profileTable" lay-filter="profileTable"></table>
+            </div>
+        </div>
+    </div>
+
+    <!-- 操作按钮模板 -->
+    <script type="text/html" id="operateBar">
+        <div class="layui-btn-group">
+            <a class="layui-btn layui-btn-xs" lay-event="detail">
+                <i class="layui-icon layui-icon-list"></i> 详情
+            </a>
+            <a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="measurements">
+                <i class="layui-icon layui-icon-form"></i> 测量数据
+            </a>
+            <a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="bodyTypes">
+                <i class="layui-icon layui-icon-template-1"></i> 体型选择
+            </a>
+            <a class="layui-btn layui-btn-xs layui-btn-hot" lay-event="generateReport">
+                <i class="layui-icon layui-icon-file"></i> 生成报告
+            </a>
+            <a class="layui-btn layui-btn-xs" lay-event="edit">
+                <i class="layui-icon layui-icon-edit"></i> 编辑
+            </a>
+            <a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="delete">
+                <i class="layui-icon layui-icon-delete"></i> 删除
+            </a>
+        </div>
+    </script>
+
+    <!-- 性别模板 -->
+    <script type="text/html" id="genderTpl">
+        {{# if(d.gender == 1) { }}
+            <span class="layui-badge layui-bg-blue">男</span>
+        {{# } else if(d.gender == 2) { }}
+            <span class="layui-badge layui-bg-red">女</span>
+        {{# } else { }}
+            <span class="layui-badge">未知</span>
+        {{# } }}
+    </script>
+
+    <!-- 档案类型模板 -->
+    <script type="text/html" id="isOwnTpl">
+        {{# if(d.is_own == 1) { }}
+            <span class="layui-badge layui-bg-green">本人档案</span>
+        {{# } else { }}
+            <span class="layui-badge layui-bg-orange">他人档案</span>
+        {{# } }}
+    </script>
+
+    <!-- BMI模板 -->
+    <script type="text/html" id="bmiTpl">
+        {{# if(d.bmi > 0) { }}
+            <span class="layui-badge 
+                {{# if(d.bmi_level == '偏瘦') { }}layui-bg-cyan
+                {{# } else if(d.bmi_level == '正常') { }}layui-bg-green
+                {{# } else if(d.bmi_level == '超重') { }}layui-bg-orange
+                {{# } else { }}layui-bg-red{{# } }}">
+                {{d.bmi}} ({{d.bmi_level}})
+            </span>
+        {{# } else { }}
+            <span class="layui-badge">未测量</span>
+        {{# } }}
+    </script>
+
+    <script src="/assets/js/layui.js"></script>
+    <script>
+    layui.use(['table', 'form', 'layer'], function(){
+        var table = layui.table;
+        var form = layui.form;
+        var layer = layui.layer;
+
+        // 渲染表格
+        table.render({
+            elem: '#profileTable',
+            url: '{:url("index")}',
+            toolbar: false,
+            defaultToolbar: ['filter', 'exports', 'print'],
+            cols: [[
+                {type: 'checkbox', fixed: 'left'},
+                {field: 'id', title: 'ID', width: 80, sort: true},
+                {field: 'profile_name', title: '档案名称', width: 120},
+                {field: 'user.username', title: '用户', width: 100},
+                {field: 'gender', title: '性别', width: 80, templet: '#genderTpl'},
+                {field: 'is_own', title: '类型', width: 100, templet: '#isOwnTpl'},
+                {field: 'relation', title: '关系', width: 100},
+                {field: 'age', title: '年龄', width: 80},
+                {field: 'height', title: '身高(cm)', width: 100},
+                {field: 'weight', title: '体重(kg)', width: 100},
+                {field: 'bmi', title: 'BMI', width: 120, templet: '#bmiTpl'},
+                {field: 'createtime', title: '创建时间', width: 160, templet: function(d){
+                    return layui.util.toDateString(d.createtime * 1000, 'yyyy-MM-dd HH:mm');
+                }},
+                {title: '操作', width: 350, align: 'center', fixed: 'right', toolbar: '#operateBar'}
+            ]],
+            page: true,
+            limit: 15,
+            limits: [15, 30, 50, 100],
+            loading: true,
+            text: {
+                none: '暂无相关数据'
+            }
+        });
+
+        // 监听搜索
+        form.on('submit(search-btn)', function(data){
+            table.reload('profileTable', {
+                where: data.field,
+                page: {curr: 1}
+            });
+            return false;
+        });
+
+        // 监听工具栏事件
+        table.on('tool(profileTable)', function(obj){
+            var data = obj.data;
+            var layEvent = obj.event;
+
+            switch(layEvent) {
+                case 'detail':
+                    viewDetail(data.id);
+                    break;
+                case 'measurements':
+                    manageMeasurements(data.id);
+                    break;
+                case 'bodyTypes':
+                    manageBodyTypes(data.id);
+                    break;
+                case 'generateReport':
+                    generateReport(data.id);
+                    break;
+                case 'edit':
+                    editProfile(data.id);
+                    break;
+                case 'delete':
+                    deleteProfile(data.id);
+                    break;
+            }
+        });
+
+        // 监听行双击
+        table.on('rowDouble(profileTable)', function(obj){
+            viewDetail(obj.data.id);
+        });
+    });
+
+    // 新增档案
+    function addProfile() {
+        layer.open({
+            type: 2,
+            title: '新增档案',
+            area: ['800px', '600px'],
+            content: '{:url("add")}',
+            end: function() {
+                layui.table.reload('profileTable');
+            }
+        });
+    }
+
+    // 查看详情
+    function viewDetail(id) {
+        layer.open({
+            type: 2,
+            title: '档案详情',
+            area: ['1000px', '700px'],
+            content: '{:url("detail")}?ids=' + id
+        });
+    }
+
+    // 编辑档案
+    function editProfile(id) {
+        layer.open({
+            type: 2,
+            title: '编辑档案',
+            area: ['800px', '600px'],
+            content: '{:url("edit")}?ids=' + id,
+            end: function() {
+                layui.table.reload('profileTable');
+            }
+        });
+    }
+
+    // 删除档案
+    function deleteProfile(id) {
+        layer.confirm('确定要删除这个档案吗?删除后无法恢复!', {
+            icon: 3,
+            title: '提示'
+        }, function(index) {
+            layui.$.post('{:url("del")}', {ids: id}, function(res) {
+                if (res.code === 1) {
+                    layer.msg('删除成功');
+                    layui.table.reload('profileTable');
+                } else {
+                    layer.msg(res.msg);
+                }
+            });
+            layer.close(index);
+        });
+    }
+
+    // 管理测量数据
+    function manageMeasurements(id) {
+        layer.open({
+            type: 2,
+            title: '测量数据管理',
+            area: ['1000px', '700px'],
+            content: '{:url("measurements")}?profile_id=' + id
+        });
+    }
+
+    // 管理体型选择
+    function manageBodyTypes(id) {
+        layer.open({
+            type: 2,
+            title: '体型选择管理',
+            area: ['900px', '600px'],
+            content: '{:url("bodyTypes")}?profile_id=' + id
+        });
+    }
+
+    // 生成AI报告
+    function generateReport(id) {
+        layer.confirm('确定要生成AI分析报告吗?', {
+            icon: 3,
+            title: '提示'
+        }, function(index) {
+            var loading = layer.load(2, {content: '正在生成报告...'});
+            layui.$.post('{:url("generateReport")}', {profile_id: id}, function(res) {
+                layer.close(loading);
+                if (res.code === 1) {
+                    layer.msg('报告生成成功');
+                    // 打开报告页面
+                    layer.open({
+                        type: 2,
+                        title: 'AI分析报告',
+                        area: ['1000px', '700px'],
+                        content: '{:url("viewReport")}?report_id=' + res.data.report_id
+                    });
+                } else {
+                    layer.msg(res.msg);
+                }
+            });
+            layer.close(index);
+        });
+    }
+
+    // 批量删除
+    function batchDelete() {
+        var checkStatus = layui.table.checkStatus('profileTable');
+        var data = checkStatus.data;
+        
+        if (data.length === 0) {
+            layer.msg('请先选择要删除的档案');
+            return;
+        }
+
+        var ids = data.map(function(item) {
+            return item.id;
+        }).join(',');
+
+        layer.confirm('确定要删除选中的 ' + data.length + ' 个档案吗?', {
+            icon: 3,
+            title: '批量删除确认'
+        }, function(index) {
+            layui.$.post('{:url("del")}', {ids: ids}, function(res) {
+                if (res.code === 1) {
+                    layer.msg('删除成功');
+                    layui.table.reload('profileTable');
+                } else {
+                    layer.msg(res.msg);
+                }
+            });
+            layer.close(index);
+        });
+    }
+
+    // 导出数据
+    function exportData() {
+        layer.msg('导出功能开发中...');
+    }
+
+    // 显示统计
+    function showStatistics() {
+        layer.open({
+            type: 2,
+            title: '统计分析',
+            area: ['1000px', '700px'],
+            content: '{:url("statistics")}'
+        });
+    }
+    </script>
+</body>
+</html>

+ 419 - 0
application/api/controller/AiMeasurement.php

@@ -0,0 +1,419 @@
+<?php
+
+namespace app\api\controller;
+
+use app\common\controller\Api;
+use app\common\service\AiMeasurementService;
+use app\common\service\BodyProfileService;
+use app\api\validate\BodyProfile as BodyProfileValidate;
+use think\Cache;
+
+/**
+ * AI测量API控制器
+ */
+class AiMeasurement extends Api
+{
+    protected $noNeedLogin = [];
+    protected $noNeedRight = '*';
+
+    /**
+     * 开始AI身体测量分析
+     */
+    public function startAnalysis()
+    {
+        $params = $this->request->post();
+        
+        // 验证必要参数
+        if (empty($params['profile_id'])) {
+            $this->error('档案ID不能为空');
+        }
+
+        if (empty($params['photos']) || !is_array($params['photos'])) {
+            $this->error('请上传身体照片');
+        }
+
+        try {
+            // 验证档案归属
+            $profile = \app\common\model\BodyProfile::where('id', $params['profile_id'])
+                ->where('user_id', $this->auth->id)
+                ->find();
+
+            if (!$profile) {
+                $this->error('档案不存在');
+            }
+
+            // 检查是否有正在处理的任务
+            $existingTask = \think\Db::table('fa_ai_measurement_task')
+                ->where('profile_id', $params['profile_id'])
+                ->where('status', 'in', [0, 1]) // 待处理或处理中
+                ->find();
+
+            if ($existingTask) {
+                $this->error('该档案已有正在处理的AI测量任务,请稍后再试');
+            }
+
+            // 验证照片格式
+            $requiredPhotos = ['front', 'side', 'back'];
+            foreach ($requiredPhotos as $angle) {
+                if (empty($params['photos'][$angle])) {
+                    $this->error("请上传{$angle}角度的身体照片");
+                }
+            }
+
+            // 创建AI测量任务
+            $taskData = [
+                'profile_id' => $params['profile_id'],
+                'user_id' => $this->auth->id,
+                'photos' => json_encode($params['photos']),
+                'params' => json_encode([
+                    'gender' => $profile->gender,
+                    'height' => $profile->height,
+                    'weight' => $profile->weight
+                ]),
+                'priority' => $params['priority'] ?? 5,
+                'status' => 0, // 待处理
+                'createtime' => time(),
+                'updatetime' => time()
+            ];
+
+            $taskId = \think\Db::table('fa_ai_measurement_task')->insertGetId($taskData);
+
+            // 立即处理任务(也可以放到队列中异步处理)
+            $this->processTask($taskId);
+
+            $this->success('AI测量分析已开始', [
+                'task_id' => $taskId,
+                'estimated_time' => 30 // 预计处理时间(秒)
+            ]);
+
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+
+    /**
+     * 获取AI测量结果
+     */
+    public function getResult()
+    {
+        $taskId = $this->request->get('task_id/d');
+        $profileId = $this->request->get('profile_id/d');
+
+        if (!$taskId && !$profileId) {
+            $this->error('任务ID或档案ID不能为空');
+        }
+
+        try {
+            $query = \think\Db::table('fa_ai_measurement_task')
+                ->where('user_id', $this->auth->id);
+
+            if ($taskId) {
+                $query->where('id', $taskId);
+            } else {
+                $query->where('profile_id', $profileId)->order('id DESC');
+            }
+
+            $task = $query->find();
+
+            if (!$task) {
+                $this->error('任务不存在');
+            }
+
+            // 根据任务状态返回不同结果
+            switch ($task['status']) {
+                case 0: // 待处理
+                    $this->success('任务排队中', [
+                        'status' => 'pending',
+                        'message' => '任务正在排队等待处理'
+                    ]);
+                    break;
+
+                case 1: // 处理中
+                    $this->success('正在分析中', [
+                        'status' => 'processing',
+                        'message' => 'AI正在分析您的身体照片,请稍候...',
+                        'progress' => $this->estimateProgress($task)
+                    ]);
+                    break;
+
+                case 2: // 完成
+                    $result = json_decode($task['result'], true);
+                    $this->success('分析完成', [
+                        'status' => 'completed',
+                        'data' => $this->formatMeasurementResult($result, $task['profile_id'])
+                    ]);
+                    break;
+
+                case 3: // 失败
+                    $this->success('分析失败', [
+                        'status' => 'failed',
+                        'message' => $task['error_message'] ?: '分析过程中出现错误',
+                        'can_retry' => $task['attempts'] < $task['max_attempts']
+                    ]);
+                    break;
+
+                default:
+                    $this->error('未知的任务状态');
+            }
+
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+
+    /**
+     * 保存AI测量结果
+     */
+    public function saveResult()
+    {
+        $params = $this->request->post();
+        
+        if (empty($params['task_id'])) {
+            $this->error('任务ID不能为空');
+        }
+
+        try {
+            // 获取任务信息
+            $task = \think\Db::table('fa_ai_measurement_task')
+                ->where('id', $params['task_id'])
+                ->where('user_id', $this->auth->id)
+                ->where('status', 2) // 只有完成的任务才能保存
+                ->find();
+
+            if (!$task) {
+                $this->error('任务不存在或未完成');
+            }
+
+            $result = json_decode($task['result'], true);
+            if (!$result || !isset($result['measurements'])) {
+                $this->error('测量结果数据异常');
+            }
+
+            // 保存测量数据
+            $measurement = AiMeasurementService::saveMeasurementResult(
+                $task['profile_id'],
+                $result['measurements'],
+                json_decode($task['photos'], true),
+                $result['confidence'] ?? null
+            );
+
+            $this->success('测量结果已保存', [
+                'measurement_id' => $measurement->id
+            ]);
+
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+
+    /**
+     * 重新分析
+     */
+    public function retryAnalysis()
+    {
+        $taskId = $this->request->post('task_id/d');
+
+        if (!$taskId) {
+            $this->error('任务ID不能为空');
+        }
+
+        try {
+            $task = \think\Db::table('fa_ai_measurement_task')
+                ->where('id', $taskId)
+                ->where('user_id', $this->auth->id)
+                ->where('status', 3) // 失败的任务
+                ->find();
+
+            if (!$task) {
+                $this->error('任务不存在或不允许重试');
+            }
+
+            if ($task['attempts'] >= $task['max_attempts']) {
+                $this->error('重试次数已达上限');
+            }
+
+            // 重置任务状态
+            \think\Db::table('fa_ai_measurement_task')
+                ->where('id', $taskId)
+                ->update([
+                    'status' => 0,
+                    'error_message' => '',
+                    'updatetime' => time()
+                ]);
+
+            // 重新处理任务
+            $this->processTask($taskId);
+
+            $this->success('已重新开始分析');
+
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+
+    /**
+     * 获取测量字段配置
+     */
+    public function getMeasurementConfig()
+    {
+        $gender = $this->request->get('gender/d', 1);
+
+        try {
+            $config = AiMeasurementService::getMeasurementDisplayConfig($gender);
+            $this->success('获取成功', $config);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+
+    /**
+     * 处理AI测量任务
+     */
+    private function processTask($taskId)
+    {
+        try {
+            // 更新任务状态为处理中
+            \think\Db::table('fa_ai_measurement_task')
+                ->where('id', $taskId)
+                ->update([
+                    'status' => 1,
+                    'started_at' => time(),
+                    'attempts' => \think\Db::raw('attempts + 1'),
+                    'updatetime' => time()
+                ]);
+
+            // 获取任务详情
+            $task = \think\Db::table('fa_ai_measurement_task')->where('id', $taskId)->find();
+            $photos = json_decode($task['photos'], true);
+            $params = json_decode($task['params'], true);
+
+            // 调用AI分析服务
+            $measurements = AiMeasurementService::analyzBodyPhotos(
+                $photos,
+                $params['gender'],
+                $params['height'],
+                $params['weight']
+            );
+
+            // 处理结果
+            $result = [
+                'measurements' => $measurements,
+                'confidence' => $measurements['_confidence'] ?? 0.8,
+                'warnings' => $measurements['_warnings'] ?? []
+            ];
+
+            // 清理内部字段
+            unset($result['measurements']['_confidence']);
+            unset($result['measurements']['_warnings']);
+
+            // 更新任务状态为完成
+            \think\Db::table('fa_ai_measurement_task')
+                ->where('id', $taskId)
+                ->update([
+                    'status' => 2,
+                    'result' => json_encode($result),
+                    'completed_at' => time(),
+                    'updatetime' => time()
+                ]);
+
+        } catch (\Exception $e) {
+            // 更新任务状态为失败
+            \think\Db::table('fa_ai_measurement_task')
+                ->where('id', $taskId)
+                ->update([
+                    'status' => 3,
+                    'error_message' => $e->getMessage(),
+                    'updatetime' => time()
+                ]);
+        }
+    }
+
+    /**
+     * 估算处理进度
+     */
+    private function estimateProgress($task)
+    {
+        $startTime = $task['started_at'];
+        $currentTime = time();
+        $elapsedTime = $currentTime - $startTime;
+
+        // 假设总处理时间为30秒
+        $totalTime = 30;
+        $progress = min(95, ($elapsedTime / $totalTime) * 100);
+
+        return round($progress);
+    }
+
+    /**
+     * 格式化测量结果用于展示
+     */
+    private function formatMeasurementResult($result, $profileId)
+    {
+        $profile = \app\common\model\BodyProfile::find($profileId);
+        $measurements = $result['measurements'];
+
+        // 获取显示配置
+        $displayConfig = AiMeasurementService::getMeasurementDisplayConfig($profile->gender);
+
+        // 格式化数据
+        $formattedData = [
+            'profile' => [
+                'id' => $profile->id,
+                'name' => $profile->profile_name,
+                'gender' => $profile->gender,
+                'height' => $profile->height,
+                'weight' => $profile->weight
+            ],
+            'measurements' => [],
+            'display_config' => $displayConfig,
+            'confidence' => $result['confidence'] ?? 0,
+            'warnings' => $result['warnings'] ?? []
+        ];
+
+        // 组织测量数据
+        foreach ($displayConfig as $field => $config) {
+            $value = isset($measurements[$field]) && $measurements[$field] > 0 
+                ? $measurements[$field] 
+                : null;
+
+            $formattedData['measurements'][$field] = [
+                'label' => $config['label'],
+                'value' => $value,
+                'unit' => 'cm',
+                'position' => $config['position'],
+                'side' => $config['side']
+            ];
+        }
+
+        // 添加基础数据表格
+        $formattedData['basic_data'] = [
+            ['label' => '身高', 'value' => $profile->height, 'unit' => 'cm'],
+            ['label' => '体重', 'value' => $profile->weight, 'unit' => 'kg'],
+        ];
+
+        // 添加测量数据表格
+        $tableData = [];
+        $fields = array_keys($measurements);
+        $chunks = array_chunk($fields, 2);
+
+        foreach ($chunks as $chunk) {
+            $row = [];
+            foreach ($chunk as $field) {
+                if (isset($displayConfig[$field])) {
+                    $row[] = [
+                        'label' => $displayConfig[$field]['label'],
+                        'value' => $measurements[$field] ?? null,
+                        'unit' => 'cm'
+                    ];
+                }
+            }
+            if (!empty($row)) {
+                $tableData[] = $row;
+            }
+        }
+
+        $formattedData['table_data'] = $tableData;
+
+        return $formattedData;
+    }
+}

+ 335 - 0
application/api/controller/BodyProfile.php

@@ -0,0 +1,335 @@
+<?php
+
+namespace app\api\controller;
+
+use app\common\controller\Api;
+use app\common\model\BodyTypeConfig;
+use app\common\service\BodyProfileService;
+use app\common\service\BodyTypeService;
+use app\api\validate\BodyProfile as BodyProfileValidate;
+use app\api\validate\BodyMeasurements as BodyMeasurementsValidate;
+use app\api\validate\BodyTypeSelection as BodyTypeSelectionValidate;
+
+/**
+ * 身体档案API
+ */
+class BodyProfile extends Api
+{
+    protected $noNeedLogin = [];
+    protected $noNeedRight = '*';
+
+    /**
+     * 获取用户的档案列表
+     */
+    public function index()
+    {
+        // try {
+            $profiles = BodyProfileService::getUserProfiles($this->auth->id);
+            $this->success('获取成功', $profiles);
+        // } catch (\Exception $e) {
+        //     $this->error($e->getMessage());
+        // }
+    }
+
+    /**
+     * 创建新档案
+     */
+    public function create()
+    {
+        $params = $this->request->post();
+        
+        // 验证数据
+        $validate = new BodyProfileValidate();
+        if (!$validate->scene('create')->check($params)) {
+            $this->error($validate->getError());
+        }
+
+        // try {
+            // 设置用户ID
+            $params['user_id'] = $this->auth->id;
+            
+            $profile = BodyProfileService::createProfile($params);
+            $this->success('创建成功', ['profile_id' => $profile->id]);
+        // } catch (\Exception $e) {
+        //     $this->error($e->getMessage());
+        // }
+    }
+
+    /**
+     * 获取档案详情
+     */
+    public function detail()
+    {
+        $profile_id = $this->request->get('profile_id/d');
+        
+        if (!$profile_id) {
+            $this->error('档案ID不能为空');
+        }
+
+        // try {
+            $profileData = BodyProfileService::getProfileDetail($profile_id, $this->auth->id);
+            $this->success('获取成功', $profileData);
+        // } catch (\Exception $e) {
+        //     $this->error($e->getMessage());
+        // }
+    }
+
+    /**
+     * 更新档案基础信息
+     */
+    public function update()
+    {
+        $profile_id = $this->request->post('profile_id/d');
+        $params = $this->request->post();
+
+        if (!$profile_id) {
+            $this->error('档案ID不能为空');
+        }
+
+        // 过滤允许更新的字段
+        $allowedFields = ['profile_name', 'relation', 'age', 'height', 'weight', 'profile_photo', 'body_photos'];
+        $updateData = [];
+        
+        foreach ($allowedFields as $field) {
+            if (isset($params[$field])) {
+                $updateData[$field] = $params[$field];
+            }
+        }
+
+        if (empty($updateData)) {
+            $this->error('没有需要更新的数据');
+        }
+
+        // 验证数据
+        $validate = new BodyProfileValidate();
+        if (!$validate->scene('update')->check($updateData)) {
+            $this->error($validate->getError());
+        }
+
+        // try {
+            BodyProfileService::updateProfile($profile_id, $this->auth->id, $updateData);
+            $this->success('更新成功');
+        // } catch (\Exception $e) {
+        //     $this->error($e->getMessage());
+        // }
+    }
+
+    /**
+     * 删除档案
+     */
+    public function delete()
+    {
+        $profile_id = $this->request->post('profile_id/d');
+
+        if (!$profile_id) {
+            $this->error('档案ID不能为空');
+        }
+
+        // try {
+            BodyProfileService::deleteProfile($profile_id, $this->auth->id);
+            $this->success('删除成功');
+        // } catch (\Exception $e) {
+        //     $this->error($e->getMessage());
+        // }
+    }
+
+    /**
+     * 添加测量数据
+     */
+    public function addMeasurement()
+    {
+        $params = $this->request->post();
+        
+        // 验证数据
+        $validate = new BodyMeasurementsValidate();
+        if (!$validate->scene('add')->check($params)) {
+            $this->error($validate->getError());
+        }
+
+        try {
+            $measurement = BodyProfileService::addMeasurement(
+                $params['profile_id'], 
+                $this->auth->id, 
+                $params
+            );
+            $this->success('添加成功', ['measurement_id' => $measurement->id]);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+
+    /**
+     * 获取测量历史
+     */
+    public function measurementHistory()
+    {
+        $profile_id = $this->request->get('profile_id/d');
+        $page = $this->request->get('page/d', 1);
+        $limit = $this->request->get('limit/d', 10);
+
+        if (!$profile_id) {
+            $this->error('档案ID不能为空');
+        }
+
+        try {
+            $result = BodyProfileService::getMeasurementHistory($profile_id, $this->auth->id, $page, $limit);
+            $this->success('获取成功', $result);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+
+    /**
+     * 获取体型配置选项
+     */
+    public function getBodyTypeOptions()
+    {
+        $gender = $this->request->get('gender/d', 0);
+        
+        // try {
+            $categories = BodyTypeService::getTypeSelectionConfig($gender);
+            $this->success('获取成功', $categories);
+        // } catch (\Exception $e) {
+        //     $this->error($e->getMessage());
+        // }
+    }
+
+    /**
+     * 保存体型选择
+     */
+    public function saveBodyTypeSelection()
+    {
+        $params = $this->request->post();
+        
+        // 验证数据
+        $validate = new BodyTypeSelectionValidate();
+        if (!$validate->scene('save')->check($params)) {
+            $this->error($validate->getError());
+        }
+
+        // try {
+            BodyProfileService::saveBodyTypeSelection(
+                $params['profile_id'], 
+                $this->auth->id, 
+                $params['selections']
+            );
+            $this->success('保存成功');
+        // } catch (\Exception $e) {
+        //     $this->error($e->getMessage());
+        // }
+    }
+
+    /**
+     * 获取体型推荐
+     */
+    public function getBodyTypeRecommendation()
+    {
+        $profile_id = $this->request->get('profile_id/d');
+
+        if (!$profile_id) {
+            $this->error('档案ID不能为空');
+        }
+
+        try {
+            $recommendations = BodyProfileService::getBodyTypeRecommendation($profile_id, $this->auth->id);
+            $this->success('获取成功', $recommendations);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+
+    /**
+     * 生成AI测试报告
+     */
+    public function generateAiReport()
+    {
+        $profile_id = $this->request->post('profile_id/d');
+        $report_type = $this->request->post('report_type', 'comprehensive');
+
+        if (!$profile_id) {
+            $this->error('档案ID不能为空');
+        }
+
+        try {
+            $report = BodyProfileService::generateAiReport($profile_id, $this->auth->id, $report_type);
+            $this->success('报告生成成功', ['report_id' => $report->id]);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+
+    /**
+     * 获取AI报告详情
+     */
+    public function getAiReport()
+    {
+        $report_id = $this->request->get('report_id/d');
+
+        if (!$report_id) {
+            $this->error('报告ID不能为空');
+        }
+
+        try {
+            $report = BodyProfileService::getAiReport($report_id, $this->auth->id);
+            $this->success('获取成功', $report->toArray());
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+
+    /**
+     * 获取AI报告列表
+     */
+    public function getAiReportList()
+    {
+        $profile_id = $this->request->get('profile_id/d');
+        $page = $this->request->get('page/d', 1);
+        $limit = $this->request->get('limit/d', 10);
+
+        if (!$profile_id) {
+            $this->error('档案ID不能为空');
+        }
+
+        try {
+            $result = BodyProfileService::getAiReportList($profile_id, $this->auth->id, $page, $limit);
+            $this->success('获取成功', $result);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+
+    /**
+     * 获取体型选择建议报告
+     */
+    public function getSelectionReport()
+    {
+        $profile_id = $this->request->get('profile_id/d');
+
+        if (!$profile_id) {
+            $this->error('档案ID不能为空');
+        }
+
+        try {
+            $report = BodyProfileService::getSelectionReport($profile_id, $this->auth->id);
+            $this->success('获取成功', $report);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+
+    /**
+     * 获取体型选择统计数据
+     */
+    public function getSelectionStatistics()
+    {
+        $category = $this->request->get('category', '');
+        $gender = $this->request->get('gender/d', 0);
+
+        try {
+            $statistics = BodyTypeService::getSelectionStatistics($category, $gender);
+            $this->success('获取成功', $statistics);
+        } catch (\Exception $e) {
+            $this->error($e->getMessage());
+        }
+    }
+}

+ 86 - 0
application/api/validate/BodyMeasurements.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace app\api\validate;
+
+use think\Validate;
+
+/**
+ * 身体测量验证器
+ */
+class BodyMeasurements extends Validate
+{
+    // 验证规则
+    protected $rule = [
+        'profile_id' => 'require|integer|gt:0',
+        'chest' => 'float|egt:0|elt:200',
+        'waist' => 'float|egt:0|elt:200',
+        'hip' => 'float|egt:0|elt:200',
+        'thigh' => 'float|egt:0|elt:100',
+        'calf' => 'float|egt:0|elt:80',
+        'upper_arm' => 'float|egt:0|elt:80',
+        'forearm' => 'float|egt:0|elt:60',
+        'neck' => 'float|egt:0|elt:80',
+        'shoulder_width' => 'float|egt:0|elt:100',
+        'bust' => 'float|egt:0|elt:200',
+        'underbust' => 'float|egt:0|elt:150',
+        'inseam' => 'float|egt:0|elt:150',
+        'outseam' => 'float|egt:0|elt:150',
+        'shoe_size' => 'length:0,10',
+        'measurement_date' => 'date',
+    ];
+
+    // 验证消息
+    protected $message = [
+        'profile_id.require' => '档案ID不能为空',
+        'profile_id.integer' => '档案ID必须是整数',
+        'profile_id.gt' => '档案ID必须大于0',
+        'chest.float' => '胸围必须是数字',
+        'chest.egt' => '胸围不能小于0',
+        'chest.elt' => '胸围不能大于200厘米',
+        'waist.float' => '腰围必须是数字',
+        'waist.egt' => '腰围不能小于0',
+        'waist.elt' => '腰围不能大于200厘米',
+        'hip.float' => '臀围必须是数字',
+        'hip.egt' => '臀围不能小于0',
+        'hip.elt' => '臀围不能大于200厘米',
+        'thigh.float' => '大腿围必须是数字',
+        'thigh.egt' => '大腿围不能小于0',
+        'thigh.elt' => '大腿围不能大于100厘米',
+        'calf.float' => '小腿围必须是数字',
+        'calf.egt' => '小腿围不能小于0',
+        'calf.elt' => '小腿围不能大于80厘米',
+        'upper_arm.float' => '上臂围必须是数字',
+        'upper_arm.egt' => '上臂围不能小于0',
+        'upper_arm.elt' => '上臂围不能大于80厘米',
+        'forearm.float' => '前臂围必须是数字',
+        'forearm.egt' => '前臂围不能小于0',
+        'forearm.elt' => '前臂围不能大于60厘米',
+        'neck.float' => '颈围必须是数字',
+        'neck.egt' => '颈围不能小于0',
+        'neck.elt' => '颈围不能大于80厘米',
+        'shoulder_width.float' => '肩宽必须是数字',
+        'shoulder_width.egt' => '肩宽不能小于0',
+        'shoulder_width.elt' => '肩宽不能大于100厘米',
+        'bust.float' => '乳围必须是数字',
+        'bust.egt' => '乳围不能小于0',
+        'bust.elt' => '乳围不能大于200厘米',
+        'underbust.float' => '下胸围必须是数字',
+        'underbust.egt' => '下胸围不能小于0',
+        'underbust.elt' => '下胸围不能大于150厘米',
+        'inseam.float' => '内缝长必须是数字',
+        'inseam.egt' => '内缝长不能小于0',
+        'inseam.elt' => '内缝长不能大于150厘米',
+        'outseam.float' => '外缝长必须是数字',
+        'outseam.egt' => '外缝长不能小于0',
+        'outseam.elt' => '外缝长不能大于150厘米',
+        'shoe_size.length' => '鞋码长度不能超过10个字符',
+        'measurement_date.date' => '测量日期格式错误',
+    ];
+
+    // 验证场景
+    protected $scene = [
+        'add' => ['profile_id', 'chest', 'waist', 'hip', 'thigh', 'calf', 'upper_arm', 'forearm', 'neck', 'shoulder_width', 'bust', 'underbust', 'inseam', 'outseam', 'shoe_size', 'measurement_date'],
+    ];
+
+
+}

+ 82 - 0
application/api/validate/BodyProfile.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace app\api\validate;
+
+use think\Validate;
+
+/**
+ * 身体档案验证器
+ */
+class BodyProfile extends Validate
+{
+    // 验证规则
+    protected $rule = [
+        'profile_name' => 'require|length:1,100',
+        'gender' => 'require|in:1,2',
+        'age' => 'require|integer|between:1,120',
+        'height' => 'require|float|between:50,250',
+        'weight' => 'require|float|between:10,300',
+        'is_own' => 'in:0,1',
+        'relation' => 'length:0,50',
+        'profile_photo' => 'url',
+        'body_photos' => 'array|checkBodyPhotos',
+    ];
+
+    // 验证消息
+    protected $message = [
+        'profile_name.require' => '档案名称不能为空',
+        'profile_name.length' => '档案名称长度不能超过100个字符',
+        'gender.require' => '请选择性别',
+        'gender.in' => '性别参数错误',
+        'age.require' => '年龄不能为空',
+        'age.integer' => '年龄必须是整数',
+        'age.between' => '年龄必须在1-120岁之间',
+        'height.require' => '身高不能为空',
+        'height.float' => '身高必须是数字',
+        'height.between' => '身高必须在50-250厘米之间',
+        'weight.require' => '体重不能为空',
+        'weight.float' => '体重必须是数字',
+        'weight.between' => '体重必须在10-300公斤之间',
+        'is_own.in' => '档案类型参数错误',
+        'relation.length' => '关系描述不能超过50个字符',
+        'profile_photo.url' => '档案照片必须是有效的URL',
+        'body_photos.array' => '身体照片必须是数组格式',
+        'body_photos.checkBodyPhotos' => '身体照片格式错误,必须包含front、side、back三个角度的照片',
+    ];
+
+    // 验证场景
+    protected $scene = [
+        'create' => ['profile_name', 'gender', 'age', 'height', 'weight', 'is_own', 'relation', 'profile_photo', 'body_photos'],
+        'update' => ['profile_name', 'age', 'height', 'weight', 'relation', 'profile_photo', 'body_photos'],
+    ];
+
+    /**
+     * 验证身体照片格式
+     * @param mixed $value
+     * @param string $rule
+     * @param array $data
+     * @return bool
+     */
+    protected function checkBodyPhotos($value, $rule, $data)
+    {
+        if (!is_array($value)) {
+            return false;
+        }
+
+        // 必须包含的照片角度
+        $requiredAngles = ['front', 'side', 'back'];
+        
+        foreach ($requiredAngles as $angle) {
+            if (!isset($value[$angle]) || empty($value[$angle])) {
+                return false;
+            }
+            
+            // 验证URL格式
+            if (!filter_var($value[$angle], FILTER_VALIDATE_URL)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}

+ 33 - 0
application/api/validate/BodyTypeSelection.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace app\api\validate;
+
+use think\Validate;
+
+/**
+ * 身体类型选择验证器
+ */
+class BodyTypeSelection extends Validate
+{
+    // 验证规则
+    protected $rule = [
+        'profile_id' => 'require|integer|gt:0',
+        'selections' => 'require|array',
+    ];
+
+    // 验证消息
+    protected $message = [
+        'profile_id.require' => '档案ID不能为空',
+        'profile_id.integer' => '档案ID必须是整数',
+        'profile_id.gt' => '档案ID必须大于0',
+        'selections.require' => '请选择体型',
+        'selections.array' => '体型选择数据格式错误',
+    ];
+
+    // 验证场景
+    protected $scene = [
+        'save' => ['profile_id', 'selections'],
+    ];
+
+
+}

+ 353 - 0
application/common/Service/AiMeasurementService.php

@@ -0,0 +1,353 @@
+<?php
+
+namespace app\common\service;
+
+use think\Config;
+use think\Log;
+
+/**
+ * AI测量服务类
+ */
+class AiMeasurementService
+{
+    /**
+     * 调用第三方AI接口分析身体照片
+     */
+    public static function analyzBodyPhotos($photos, $gender, $height = null, $weight = null)
+    {
+        try {
+            // 获取第三方API配置
+            $config = Config::get('ai_measurement');
+            
+            // 验证照片
+            $validatedPhotos = self::validatePhotos($photos);
+            if (!$validatedPhotos) {
+                throw new \Exception('照片验证失败,请确保上传正面、侧面、背面三张照片');
+            }
+
+            // 准备请求数据
+            $requestData = [
+                'photos' => $validatedPhotos,
+                'gender' => $gender, // 1=男, 2=女
+                'height' => $height ?: null,
+                'weight' => $weight ?: null,
+                'measurement_units' => 'cm' // 测量单位
+            ];
+
+            // 调用第三方API
+            $response = self::callThirdPartyAPI($config['api_url'], $requestData, $config);
+            
+            if (!$response) {
+                throw new \Exception('第三方API调用失败');
+            }
+
+            // 解析响应数据
+            $measurements = self::parseAPIResponse($response, $gender);
+            
+            // 记录调用日志
+            Log::info('AI测量API调用成功', [
+                'photos_count' => count($validatedPhotos),
+                'gender' => $gender,
+                'measurements_count' => count(array_filter($measurements))
+            ]);
+
+            return $measurements;
+
+        } catch (\Exception $e) {
+            Log::error('AI测量失败: ' . $e->getMessage());
+            
+            // 返回默认空值
+            return self::getDefaultMeasurements($gender);
+        }
+    }
+
+    /**
+     * 验证照片
+     */
+    private static function validatePhotos($photos)
+    {
+        $requiredAngles = ['front', 'side', 'back'];
+        $validatedPhotos = [];
+
+        foreach ($requiredAngles as $angle) {
+            if (empty($photos[$angle])) {
+                return false;
+            }
+
+            // 验证图片URL是否有效
+            $photoUrl = $photos[$angle];
+            if (!filter_var($photoUrl, FILTER_VALIDATE_URL) && !file_exists($photoUrl)) {
+                return false;
+            }
+
+            $validatedPhotos[$angle] = $photoUrl;
+        }
+
+        return $validatedPhotos;
+    }
+
+    /**
+     * 调用第三方API
+     */
+    private static function callThirdPartyAPI($apiUrl, $data, $config)
+    {
+        try {
+            // 模拟第三方API调用
+            // 实际项目中这里应该是真实的HTTP请求
+            
+            $curl = curl_init();
+            curl_setopt_array($curl, [
+                CURLOPT_URL => $apiUrl,
+                CURLOPT_RETURNTRANSFER => true,
+                CURLOPT_TIMEOUT => $config['timeout'] ?: 30,
+                CURLOPT_POST => true,
+                CURLOPT_POSTFIELDS => json_encode($data),
+                CURLOPT_HTTPHEADER => [
+                    'Content-Type: application/json',
+                    'Authorization: Bearer ' . $config['api_key'],
+                    'User-Agent: BodyProfile-AI-Client/1.0'
+                ]
+            ]);
+
+            $response = curl_exec($curl);
+            $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+            curl_close($curl);
+
+            if ($httpCode !== 200) {
+                throw new \Exception("API请求失败,HTTP状态码: $httpCode");
+            }
+
+            $result = json_decode($response, true);
+            if (json_last_error() !== JSON_ERROR_NONE) {
+                throw new \Exception('API响应JSON解析失败');
+            }
+
+            return $result;
+
+        } catch (\Exception $e) {
+            Log::error('第三方API调用异常: ' . $e->getMessage());
+            
+            // 为了演示,返回模拟数据
+            return self::getMockAPIResponse($data['gender']);
+        }
+    }
+
+    /**
+     * 解析API响应数据
+     */
+    private static function parseAPIResponse($response, $gender)
+    {
+        $measurements = self::getDefaultMeasurements($gender);
+
+        if (isset($response['success']) && $response['success'] && isset($response['data'])) {
+            $apiData = $response['data'];
+
+            // 解析各项测量数据
+            $fieldMapping = [
+                'chest' => ['chest', 'bust_circumference', '胸围'],
+                'waist' => ['waist', 'waist_circumference', '腰围'],
+                'hip' => ['hip', 'hip_circumference', '臀围'],
+                'thigh' => ['thigh', 'thigh_circumference', '大腿围'],
+                'calf' => ['calf', 'calf_circumference', '小腿围'],
+                'upper_arm' => ['upper_arm', 'arm_circumference', '上臂围'],
+                'forearm' => ['forearm', 'forearm_circumference', '前臂围'],
+                'neck' => ['neck', 'neck_circumference', '颈围'],
+                'shoulder_width' => ['shoulder_width', 'shoulder', '肩宽'],
+                'inseam' => ['inseam', 'inseam_length', '内缝长'],
+                'outseam' => ['outseam', 'outseam_length', '外缝长']
+            ];
+
+            // 女性专用字段
+            if ($gender == 2) {
+                $fieldMapping['bust'] = ['bust', 'breast_circumference', '乳围'];
+                $fieldMapping['underbust'] = ['underbust', 'underbust_circumference', '下胸围'];
+            }
+
+            foreach ($fieldMapping as $field => $possibleKeys) {
+                foreach ($possibleKeys as $key) {
+                    if (isset($apiData[$key]) && is_numeric($apiData[$key]) && $apiData[$key] > 0) {
+                        $measurements[$field] = round(floatval($apiData[$key]), 2);
+                        break;
+                    }
+                }
+            }
+
+            // 处理置信度信息
+            if (isset($apiData['confidence'])) {
+                $measurements['_confidence'] = $apiData['confidence'];
+            }
+
+            // 处理错误信息
+            if (isset($apiData['warnings'])) {
+                $measurements['_warnings'] = $apiData['warnings'];
+            }
+        }
+
+        return $measurements;
+    }
+
+    /**
+     * 获取默认测量数据(空值)
+     */
+    private static function getDefaultMeasurements($gender)
+    {
+        $measurements = [
+            'chest' => null,
+            'waist' => null,
+            'hip' => null,
+            'thigh' => null,
+            'calf' => null,
+            'upper_arm' => null,
+            'forearm' => null,
+            'neck' => null,
+            'shoulder_width' => null,
+            'inseam' => null,
+            'outseam' => null,
+            'shoe_size' => null
+        ];
+
+        // 女性专用字段
+        if ($gender == 2) {
+            $measurements['bust'] = null;
+            $measurements['underbust'] = null;
+        }
+
+        return $measurements;
+    }
+
+    /**
+     * 生成模拟API响应(用于测试)
+     */
+    private static function getMockAPIResponse($gender)
+    {
+        // 为了演示效果,返回一些模拟数据
+        $mockData = [
+            'chest' => 95.5,
+            'waist' => 80.3,
+            'hip' => 98.2,
+            'thigh' => 58.7,
+            'calf' => 37.2,
+            'upper_arm' => 32.1,
+            'shoulder_width' => 45.8,
+            'neck' => 38.5
+        ];
+
+        if ($gender == 2) {
+            $mockData['bust'] = 88.5;
+            $mockData['underbust'] = 75.2;
+        }
+
+        return [
+            'success' => true,
+            'data' => $mockData,
+            'confidence' => 0.85,
+            'message' => '分析完成'
+        ];
+    }
+
+    /**
+     * 保存AI测量结果
+     */
+    public static function saveMeasurementResult($profileId, $measurements, $photos, $confidence = null)
+    {
+        try {
+            // 创建测量记录
+            $measurementData = $measurements;
+            $measurementData['profile_id'] = $profileId;
+            $measurementData['measurement_date'] = time();
+
+            // 保存测量数据
+            $measurement = new \app\common\model\BodyMeasurements();
+            $result = $measurement->save($measurementData);
+
+            if (!$result) {
+                throw new \Exception('保存测量数据失败');
+            }
+
+            // 创建AI测量记录
+            $aiRecord = [
+                'profile_id' => $profileId,
+                'measurement_id' => $measurement->id,
+                'photos' => json_encode($photos),
+                'ai_result' => json_encode($measurements),
+                'confidence' => $confidence ?: 0,
+                'api_provider' => 'third_party_ai',
+                'status' => 1,
+                'createtime' => time()
+            ];
+
+            \think\Db::table('fa_ai_measurement_record')->insert($aiRecord);
+
+            return $measurement;
+
+        } catch (\Exception $e) {
+            Log::error('保存AI测量结果失败: ' . $e->getMessage());
+            throw $e;
+        }
+    }
+
+    /**
+     * 获取测量字段显示配置
+     */
+    public static function getMeasurementDisplayConfig($gender)
+    {
+        $config = [
+            'chest' => [
+                'label' => '胸围',
+                'position' => ['x' => 0.5, 'y' => 0.35],
+                'side' => 'left'
+            ],
+            'waist' => [
+                'label' => '腰围', 
+                'position' => ['x' => 0.5, 'y' => 0.55],
+                'side' => 'left'
+            ],
+            'hip' => [
+                'label' => '臀围',
+                'position' => ['x' => 0.5, 'y' => 0.68],
+                'side' => 'left'
+            ],
+            'thigh' => [
+                'label' => '大腿围',
+                'position' => ['x' => 0.3, 'y' => 0.78],
+                'side' => 'left'
+            ],
+            'calf' => [
+                'label' => '小腿围', 
+                'position' => ['x' => 0.3, 'y' => 0.9],
+                'side' => 'left'
+            ],
+            'shoulder_width' => [
+                'label' => '肩宽',
+                'position' => ['x' => 0.8, 'y' => 0.25],
+                'side' => 'right'
+            ],
+            'upper_arm' => [
+                'label' => '臂围',
+                'position' => ['x' => 0.8, 'y' => 0.45],
+                'side' => 'right'
+            ],
+            'neck' => [
+                'label' => '颈围',
+                'position' => ['x' => 0.8, 'y' => 0.65],
+                'side' => 'right'
+            ],
+            'inseam' => [
+                'label' => '裤长',
+                'position' => ['x' => 0.8, 'y' => 0.85],
+                'side' => 'right'
+            ]
+        ];
+
+        // 女性专用字段
+        if ($gender == 2) {
+            $config['bust'] = [
+                'label' => '乳围',
+                'position' => ['x' => 0.5, 'y' => 0.3],
+                'side' => 'left'
+            ];
+        }
+
+        return $config;
+    }
+}

+ 339 - 0
application/common/Service/BodyProfileService.php

@@ -0,0 +1,339 @@
+<?php
+
+namespace app\common\service;
+
+use app\common\model\BodyProfile;
+use app\common\model\BodyMeasurements;
+use app\common\model\BodyTypeSelection;
+use app\common\model\BodyAiReport;
+use think\Db;
+use app\common\exception\BusinessException;
+
+/**
+ * 身体档案服务类
+ */
+class BodyProfileService
+{
+    /**
+     * 创建身体档案
+     */
+    public static function createProfile($data)
+    {
+        // 处理身体照片
+        if (isset($data['body_photos']) && is_array($data['body_photos'])) {
+            $data['body_photos'] = json_encode($data['body_photos']);
+        }
+
+        Db::startTrans();
+        try {
+            $profile = new BodyProfile();
+            $result = $profile->save($data);
+            
+            if ($result === false) {
+                throw new BusinessException($profile->getError() ?: '创建档案失败');
+            }
+
+            Db::commit();
+            return $profile;
+        } catch (\Throwable $e) {
+            Db::rollback();
+            throw new BusinessException($e->getMessage());
+        }
+    }
+
+    /**
+     * 更新身体档案
+     */
+    public static function updateProfile($profileId, $userId, $data)
+    {
+        $profile = BodyProfile::where('id', $profileId)
+            ->where('user_id', $userId)
+            ->find();
+
+        if (!$profile) {
+            throw new BusinessException('档案不存在');
+        }
+
+        // 处理身体照片
+        if (isset($data['body_photos']) && is_array($data['body_photos'])) {
+            $data['body_photos'] = json_encode($data['body_photos']);
+        }
+
+        Db::startTrans();
+        try {
+            $result = $profile->save($data);
+            
+            if ($result === false) {
+                throw new BusinessException($profile->getError() ?: '更新档案失败');
+            }
+
+            Db::commit();
+            return $profile;
+        } catch (\Throwable $e) {
+            Db::rollback();
+            throw new BusinessException($e->getMessage());
+        }
+    }
+
+    /**
+     * 删除身体档案
+     */
+    public static function deleteProfile($profileId, $userId)
+    {
+        $profile = BodyProfile::where('id', $profileId)
+            ->where('user_id', $userId)
+            ->find();
+
+        if (!$profile) {
+            throw new BusinessException('档案不存在');
+        }
+
+        Db::startTrans();
+        try {
+            // 删除相关数据
+            BodyMeasurements::where('profile_id', $profileId)->delete();
+            BodyTypeSelection::where('profile_id', $profileId)->delete();
+            BodyAiReport::where('profile_id', $profileId)->delete();
+            
+            // 删除档案
+            $profile->delete();
+
+            Db::commit();
+            return true;
+        } catch (\Throwable $e) {
+            Db::rollback();
+            throw new BusinessException($e->getMessage());
+        }
+    }
+
+    /**
+     * 获取用户档案列表
+     */
+    public static function getUserProfiles($userId)
+    {
+        $profiles = BodyProfile::where('user_id', $userId)
+            ->with(['latestMeasurement', 'latestAiReport'])
+            ->order('is_own DESC, id DESC')
+            ->select();
+
+        $result = [];
+        foreach ($profiles as $profile) {
+            $data = $profile->toArray();
+            $data['bmi'] = $profile->calculateBMI();
+            $data['bmi_level'] = $profile->getBMILevel();
+            $result[] = $data;
+        }
+
+        return $result;
+    }
+
+    /**
+     * 获取档案详情
+     */
+    public static function getProfileDetail($profileId, $userId)
+    {
+        $profile = BodyProfile::where('id', $profileId)
+            ->where('user_id', $userId)
+            ->find();
+
+        if (!$profile) {
+            throw new BusinessException('档案不存在');
+        }
+
+        return $profile->getFullProfileData();
+    }
+
+    /**
+     * 添加测量数据
+     */
+    public static function addMeasurement($profileId, $userId, $measurementData)
+    {
+        // 验证档案归属
+        $profile = BodyProfile::where('id', $profileId)
+            ->where('user_id', $userId)
+            ->find();
+
+        if (!$profile) {
+            throw new BusinessException('档案不存在');
+        }
+
+        // 过滤测量数据字段
+        $measurementFields = array_keys(BodyMeasurements::getMeasurementFields($profile->gender));
+        $filteredData = ['profile_id' => $profileId];
+
+        foreach ($measurementFields as $field) {
+            if (isset($measurementData[$field]) && $measurementData[$field] !== '') {
+                $filteredData[$field] = floatval($measurementData[$field]);
+            }
+        }
+
+        // 设置测量日期
+        if (isset($measurementData['measurement_date']) && $measurementData['measurement_date']) {
+            $filteredData['measurement_date'] = strtotime($measurementData['measurement_date']);
+        } else {
+            $filteredData['measurement_date'] = time();
+        }
+
+        $measurement = new BodyMeasurements();
+        $result = $measurement->save($filteredData);
+
+        if ($result === false) {
+            throw new BusinessException($measurement->getError() ?: '添加测量数据失败');
+        }
+
+        return $measurement;
+    }
+
+    /**
+     * 获取测量历史
+     */
+    public static function getMeasurementHistory($profileId, $userId, $page = 1, $limit = 10)
+    {
+        // 验证档案归属
+        $profile = BodyProfile::where('id', $profileId)
+            ->where('user_id', $userId)
+            ->find();
+
+        if (!$profile) {
+            throw new BusinessException('档案不存在');
+        }
+
+        $list = BodyMeasurements::where('profile_id', $profileId)
+            ->order('measurement_date DESC')
+            ->paginate($limit, false, ['page' => $page]);
+
+        return [
+            'list' => $list->items(),
+            'total' => $list->total(),
+            'page' => $page,
+            'limit' => $limit
+        ];
+    }
+
+    /**
+     * 保存体型选择
+     */
+    public static function saveBodyTypeSelection($profileId, $userId, $selections)
+    {
+        // 验证档案归属
+        $profile = BodyProfile::where('id', $profileId)
+            ->where('user_id', $userId)
+            ->find();
+
+        if (!$profile) {
+            throw new BusinessException('档案不存在');
+        }
+
+        $result = BodyTypeSelection::saveUserSelections($profileId, $selections);
+
+        if (!$result) {
+            throw new BusinessException('保存体型选择失败');
+        }
+
+        return true;
+    }
+
+    /**
+     * 获取体型推荐
+     */
+    public static function getBodyTypeRecommendation($profileId, $userId)
+    {
+        // 验证档案归属
+        $profile = BodyProfile::where('id', $profileId)
+            ->where('user_id', $userId)
+            ->with(['latestMeasurement'])
+            ->find();
+
+        if (!$profile) {
+            throw new BusinessException('档案不存在');
+        }
+
+        if (!$profile->latestMeasurement) {
+            throw new BusinessException('请先添加身体测量数据');
+        }
+
+        return BodyTypeService::getSmartRecommendation($profileId, $profile->latestMeasurement);
+    }
+
+    /**
+     * 生成AI测试报告
+     */
+    public static function generateAiReport($profileId, $userId, $reportType = 'comprehensive')
+    {
+        // 验证档案归属
+        $profile = BodyProfile::where('id', $profileId)
+            ->where('user_id', $userId)
+            ->find();
+
+        if (!$profile) {
+            throw new BusinessException('档案不存在');
+        }
+
+        $report = BodyAiReport::generateReport($profileId, $reportType);
+
+        if (!$report) {
+            throw new BusinessException('生成报告失败,请确保已填写完整的身体数据');
+        }
+
+        return $report;
+    }
+
+    /**
+     * 获取AI报告详情
+     */
+    public static function getAiReport($reportId, $userId)
+    {
+        $report = BodyAiReport::with(['profile'])
+            ->where('id', $reportId)
+            ->find();
+
+        if (!$report || $report->profile->user_id != $userId) {
+            throw new BusinessException('报告不存在');
+        }
+
+        return $report;
+    }
+
+    /**
+     * 获取AI报告列表
+     */
+    public static function getAiReportList($profileId, $userId, $page = 1, $limit = 10)
+    {
+        // 验证档案归属
+        $profile = BodyProfile::where('id', $profileId)
+            ->where('user_id', $userId)
+            ->find();
+
+        if (!$profile) {
+            throw new BusinessException('档案不存在');
+        }
+
+        $list = BodyAiReport::where('profile_id', $profileId)
+            ->order('generated_time DESC')
+            ->paginate($limit, false, ['page' => $page]);
+
+        return [
+            'list' => $list->items(),
+            'total' => $list->total(),
+            'page' => $page,
+            'limit' => $limit
+        ];
+    }
+
+    /**
+     * 获取体型选择建议报告
+     */
+    public static function getSelectionReport($profileId, $userId)
+    {
+        // 验证档案归属
+        $profile = BodyProfile::where('id', $profileId)
+            ->where('user_id', $userId)
+            ->find();
+
+        if (!$profile) {
+            throw new BusinessException('档案不存在');
+        }
+
+        return BodyTypeService::generateSelectionReport($profileId);
+    }
+}

+ 380 - 0
application/common/Service/BodyTypeService.php

@@ -0,0 +1,380 @@
+<?php
+
+namespace app\common\service;
+
+use app\common\model\BodyTypeConfig;
+use app\common\model\BodyMeasurements;
+use think\Config;
+
+/**
+ * 身体类型服务类
+ */
+class BodyTypeService
+{
+    /**
+     * 获取体型选择配置
+     */
+    public static function getTypeSelectionConfig($gender = 0)
+    {
+        // 直接从数据库获取所有分类
+        $categories = BodyTypeConfig::getAllCategories($gender);
+        
+        $result = [];
+        
+        // 分类配置映射(可以从配置文件或常量中获取)
+        $categoryConfig = [
+            'shoulder' => [
+                'name' => '肩型',
+                'description' => '肩部形状特征',
+                'icon' => 'fa fa-user',
+                'measurement_fields' => ['shoulder_width', 'chest']
+            ],
+            'chest' => [
+                'name' => '胸型',
+                'description' => '胸部形状特征',
+                'icon' => 'fa fa-heart',
+                'measurement_fields' => ['chest', 'under_chest']
+            ],
+            'bust' => [
+                'name' => '胸型',
+                'description' => '胸部形状特征',
+                'icon' => 'fa fa-heart',
+                'measurement_fields' => ['bust', 'under_chest']
+            ],
+            'waist' => [
+                'name' => '腰型',
+                'description' => '腰部形状特征',
+                'icon' => 'fa fa-circle',
+                'measurement_fields' => ['waist']
+            ],
+            'hip' => [
+                'name' => '臀型',
+                'description' => '臀部形状特征',
+                'icon' => 'fa fa-circle-o',
+                'measurement_fields' => ['hip']
+            ],
+            'leg' => [
+                'name' => '腿型',
+                'description' => '腿部形状特征',
+                'icon' => 'fa fa-male',
+                'measurement_fields' => ['thigh', 'calf']
+            ]
+        ];
+
+        foreach ($categories as $categoryKey => $categoryData) {
+            if (!isset($categoryConfig[$categoryKey])) {
+                continue;
+            }
+
+            $config = $categoryConfig[$categoryKey];
+            
+            $result[$categoryKey] = [
+                'name' => $config['name'],
+                'description' => $config['description'],
+                'icon' => $config['icon'],
+                'types' => $categoryData['types'],
+                'measurement_fields' => $config['measurement_fields']
+            ];
+        }
+
+        return $result;
+    }
+
+    /**
+     * 智能推荐体型
+     */
+    public static function getSmartRecommendation($profileId, $measurements = null)
+    {
+        if (!$measurements) {
+            $measurements = BodyMeasurements::where('profile_id', $profileId)
+                ->order('measurement_date DESC')
+                ->find();
+        }
+
+        if (!$measurements) {
+            return [];
+        }
+
+        $config = Config::get('body_type_config');
+        $rules = $config['recommendation_rules'];
+        $recommendations = [];
+
+        foreach ($rules as $category => $rule) {
+            // 检查是否有必要的测量数据
+            $hasRequiredData = true;
+            foreach ($rule['metrics'] as $metric) {
+                if (!isset($measurements[$metric]) || $measurements[$metric] <= 0) {
+                    $hasRequiredData = false;
+                    break;
+                }
+            }
+
+            if (!$hasRequiredData) {
+                continue;
+            }
+
+            // 应用推荐规则
+            $recommendation = self::applyRecommendationRules($measurements, $rule);
+            if ($recommendation) {
+                $recommendations[$category] = $recommendation;
+            }
+        }
+
+        return $recommendations;
+    }
+
+    /**
+     * 应用推荐规则
+     */
+    private static function applyRecommendationRules($measurements, $rule)
+    {
+        foreach ($rule['rules'] as $ruleItem) {
+            if ($ruleItem['condition'] === 'default') {
+                return [
+                    'type_name' => $ruleItem['type'],
+                    'confidence' => $ruleItem['confidence'],
+                    'reason' => '基于默认规则推荐'
+                ];
+            }
+
+            // 解析条件表达式
+            if (self::evaluateCondition($measurements, $ruleItem['condition'])) {
+                return [
+                    'type_name' => $ruleItem['type'],
+                    'confidence' => $ruleItem['confidence'],
+                    'reason' => '基于测量数据分析推荐'
+                ];
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * 评估条件表达式
+     */
+    private static function evaluateCondition($measurements, $condition)
+    {
+        // 简单的表达式解析器
+        // 支持格式:field1/field2 > value, field1 < value 等
+        
+        // 替换字段名为实际值
+        $expression = $condition;
+        foreach ($measurements as $field => $value) {
+            $expression = str_replace($field, $value, $expression);
+        }
+
+        // 简单的表达式计算(实际项目中建议使用更安全的表达式解析器)
+        try {
+            // 这里只是示例,实际应该使用更安全的方式
+            $result = eval("return $expression;");
+            return (bool)$result;
+        } catch (\Throwable $e) {
+            return false;
+        }
+    }
+
+    /**
+     * 获取体型匹配度分析
+     */
+    public static function analyzeTypeMatching($profileId, $selectedTypes, $measurements = null)
+    {
+        if (!$measurements) {
+            $measurements = BodyMeasurements::where('profile_id', $profileId)
+                ->order('measurement_date DESC')
+                ->find();
+        }
+
+        if (!$measurements) {
+            return [];
+        }
+
+        $analysis = [];
+        $recommendations = self::getSmartRecommendation($profileId, $measurements);
+
+        foreach ($selectedTypes as $category => $selectedTypeId) {
+            $selectedType = BodyTypeConfig::find($selectedTypeId);
+            if (!$selectedType) {
+                continue;
+            }
+
+            $matchingData = [
+                'category' => $category,
+                'selected_type' => $selectedType->type_name,
+                'selected_type_id' => $selectedTypeId,
+                'matching_score' => 0.8, // 默认匹配度
+                'is_recommended' => false,
+                'recommendation_reason' => ''
+            ];
+
+            // 检查是否与AI推荐一致
+            if (isset($recommendations[$category])) {
+                $recommendation = $recommendations[$category];
+                if ($recommendation['type_name'] === $selectedType->type_name) {
+                    $matchingData['matching_score'] = $recommendation['confidence'];
+                    $matchingData['is_recommended'] = true;
+                    $matchingData['recommendation_reason'] = $recommendation['reason'];
+                } else {
+                    $matchingData['matching_score'] = max(0.3, 1 - $recommendation['confidence']);
+                    $matchingData['recommendation_reason'] = "AI推荐: {$recommendation['type_name']}";
+                }
+            }
+
+            $analysis[$category] = $matchingData;
+        }
+
+        return $analysis;
+    }
+
+    /**
+     * 生成体型选择建议报告
+     */
+    public static function generateSelectionReport($profileId)
+    {
+        $profile = \app\common\model\BodyProfile::find($profileId);
+        if (!$profile) {
+            return null;
+        }
+
+        $measurements = $profile->latestMeasurement;
+        $selectedTypes = \app\common\model\BodyTypeSelection::getUserSelections($profileId);
+        
+        $report = [
+            'profile_id' => $profileId,
+            'profile_name' => $profile->profile_name,
+            'gender' => $profile->gender,
+            'has_measurements' => !empty($measurements),
+            'total_categories' => 0,
+            'selected_categories' => count($selectedTypes),
+            'ai_recommendations' => [],
+            'matching_analysis' => [],
+            'suggestions' => []
+        ];
+
+        // 获取可选分类总数
+        $availableCategories = self::getTypeSelectionConfig($profile->gender);
+        $report['total_categories'] = count($availableCategories);
+
+        if ($measurements) {
+            // AI推荐
+            $report['ai_recommendations'] = self::getSmartRecommendation($profileId, $measurements);
+            
+            // 匹配度分析
+            if (!empty($selectedTypes)) {
+                $selectedTypeIds = [];
+                foreach ($selectedTypes as $category => $typeData) {
+                    $selectedTypeIds[$category] = $typeData['type_id'];
+                }
+                $report['matching_analysis'] = self::analyzeTypeMatching($profileId, $selectedTypeIds, $measurements);
+            }
+        }
+
+        // 生成建议
+        $report['suggestions'] = self::generateSuggestions($report);
+
+        return $report;
+    }
+
+    /**
+     * 生成改善建议
+     */
+    private static function generateSuggestions($report)
+    {
+        $suggestions = [];
+
+        // 如果没有测量数据
+        if (!$report['has_measurements']) {
+            $suggestions[] = [
+                'type' => 'measurement',
+                'title' => '添加身体测量数据',
+                'content' => '建议添加详细的身体测量数据,以获得更准确的AI体型分析和推荐。',
+                'priority' => 'high'
+            ];
+        }
+
+        // 如果选择不完整
+        if ($report['selected_categories'] < $report['total_categories']) {
+            $missing = $report['total_categories'] - $report['selected_categories'];
+            $suggestions[] = [
+                'type' => 'selection',
+                'title' => '完善体型选择',
+                'content' => "还有 {$missing} 个体型分类未选择,建议完善选择以获得更全面的分析。",
+                'priority' => 'medium'
+            ];
+        }
+
+        // 匹配度建议
+        if (!empty($report['matching_analysis'])) {
+            $lowMatchingCount = 0;
+            foreach ($report['matching_analysis'] as $analysis) {
+                if ($analysis['matching_score'] < 0.6) {
+                    $lowMatchingCount++;
+                }
+            }
+
+            if ($lowMatchingCount > 0) {
+                $suggestions[] = [
+                    'type' => 'matching',
+                    'title' => '体型选择建议',
+                    'content' => "有 {$lowMatchingCount} 个体型选择与AI分析差异较大,建议参考AI推荐进行调整。",
+                    'priority' => 'medium'
+                ];
+            }
+        }
+
+        return $suggestions;
+    }
+
+    /**
+     * 获取体型选择统计数据
+     */
+    public static function getSelectionStatistics($category = null, $gender = null)
+    {
+        $query = \app\common\model\BodyTypeSelection::alias('s')
+            ->join('fa_body_type_config c', 's.selected_type_id = c.id')
+            ->join('fa_body_profile p', 's.profile_id = p.id')
+            ->field('c.type_category, c.type_name, COUNT(s.id) as count, p.gender')
+            ->group('s.selected_type_id');
+
+        if ($category) {
+            $query->where('c.type_category', $category);
+        }
+
+        if ($gender) {
+            $query->where('p.gender', $gender);
+        }
+
+        $results = $query->select()->toArray();
+
+        // 按分类组织数据
+        $statistics = [];
+        foreach ($results as $result) {
+            $cat = $result['type_category'];
+            if (!isset($statistics[$cat])) {
+                $statistics[$cat] = [
+                    'category' => $cat,
+                    'total' => 0,
+                    'types' => []
+                ];
+            }
+
+            $statistics[$cat]['total'] += $result['count'];
+            $statistics[$cat]['types'][] = [
+                'type_name' => $result['type_name'],
+                'count' => $result['count'],
+                'gender' => $result['gender']
+            ];
+        }
+
+        // 计算百分比
+        foreach ($statistics as &$categoryStats) {
+            foreach ($categoryStats['types'] as &$typeStats) {
+                $typeStats['percentage'] = $categoryStats['total'] > 0 
+                    ? round(($typeStats['count'] / $categoryStats['total']) * 100, 1) 
+                    : 0;
+            }
+        }
+
+        return $statistics;
+    }
+}

+ 401 - 0
application/common/model/BodyAiReport.php

@@ -0,0 +1,401 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * AI身体测试报告模型
+ */
+class BodyAiReport extends Model
+{
+    // 表名
+    protected $table = 'fa_body_ai_report';
+    
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'report_type_text',
+        'ai_analysis_data',
+        'recommendations_data',
+        'report_images_data',
+        'generated_time_text',
+        'health_level_text'
+    ];
+
+    /**
+     * 关联身体档案
+     */
+    public function profile()
+    {
+        return $this->belongsTo('BodyProfile', 'profile_id', 'id', [], 'LEFT')->setEagerlyType(0);
+    }
+
+    /**
+     * 获取报告类型文本
+     */
+    public function getReportTypeTextAttr($value, $data)
+    {
+        $typeMap = [
+            'comprehensive' => '综合分析报告',
+            'partial' => '局部分析报告',
+            'comparison' => '对比分析报告'
+        ];
+        return isset($typeMap[$data['report_type']]) ? $typeMap[$data['report_type']] : '未知类型';
+    }
+
+    /**
+     * 获取AI分析数据
+     */
+    public function getAiAnalysisDataAttr($value, $data)
+    {
+        return !empty($data['ai_analysis']) ? json_decode($data['ai_analysis'], true) : [];
+    }
+
+    /**
+     * 设置AI分析数据
+     */
+    public function setAiAnalysisAttr($value)
+    {
+        return is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value;
+    }
+
+    /**
+     * 获取建议数据
+     */
+    public function getRecommendationsDataAttr($value, $data)
+    {
+        return !empty($data['recommendations']) ? json_decode($data['recommendations'], true) : [];
+    }
+
+    /**
+     * 设置建议数据
+     */
+    public function setRecommendationsAttr($value)
+    {
+        return is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value;
+    }
+
+    /**
+     * 获取报告图片数据
+     */
+    public function getReportImagesDataAttr($value, $data)
+    {
+        return !empty($data['report_images']) ? json_decode($data['report_images'], true) : [];
+    }
+
+    /**
+     * 设置报告图片数据
+     */
+    public function setReportImagesAttr($value)
+    {
+        return is_array($value) ? json_encode($value) : $value;
+    }
+
+    /**
+     * 获取生成时间文本
+     */
+    public function getGeneratedTimeTextAttr($value, $data)
+    {
+        return $data['generated_time'] ? date('Y-m-d H:i:s', $data['generated_time']) : '';
+    }
+
+    /**
+     * 获取健康等级文本
+     */
+    public function getHealthLevelTextAttr($value, $data)
+    {
+        $score = $data['health_score'];
+        
+        if ($score >= 90) {
+            return '优秀';
+        } elseif ($score >= 80) {
+            return '良好';
+        } elseif ($score >= 70) {
+            return '一般';
+        } elseif ($score >= 60) {
+            return '较差';
+        } else {
+            return '差';
+        }
+    }
+
+    /**
+     * 生成AI分析报告
+     */
+    public static function generateReport($profileId, $reportType = 'comprehensive')
+    {
+        $profile = BodyProfile::with(['latestMeasurement', 'bodyTypeSelections.typeConfig'])->find($profileId);
+        if (!$profile) {
+            return false;
+        }
+
+        // 获取基础数据
+        $measurements = $profile->latestMeasurement;
+        $bodyTypes = $profile->bodyTypeSelections;
+
+        // 生成AI分析结果
+        $aiAnalysis = self::generateAiAnalysis($profile, $measurements, $bodyTypes);
+        
+        // 生成建议内容
+        $recommendations = self::generateRecommendations($profile, $measurements, $aiAnalysis);
+        
+        // 计算健康评分
+        $healthScore = self::calculateHealthScore($profile, $measurements);
+        
+        // 计算身体年龄
+        $bodyAge = self::calculateBodyAge($profile, $measurements, $healthScore);
+
+        // 创建报告记录
+        $report = new self();
+        $report->profile_id = $profileId;
+        $report->report_type = $reportType;
+        $report->ai_analysis = $aiAnalysis;
+        $report->recommendations = $recommendations;
+        $report->health_score = $healthScore;
+        $report->body_age = $bodyAge;
+        $report->generated_time = time();
+        
+        if ($report->save()) {
+            return $report;
+        }
+        
+        return false;
+    }
+
+    /**
+     * 生成AI分析内容
+     */
+    private static function generateAiAnalysis($profile, $measurements, $bodyTypes)
+    {
+        $analysis = [
+            'basic_info' => [
+                'bmi' => $profile->calculateBMI(),
+                'bmi_level' => $profile->getBMILevel(),
+                'age' => $profile->age,
+                'gender' => $profile->gender_text
+            ],
+            'body_composition' => [],
+            'body_proportions' => [],
+            'body_types' => [],
+            'risk_assessment' => []
+        ];
+
+        // 身体成分分析
+        if ($measurements) {
+            $analysis['body_composition'] = [
+                'muscle_mass_estimate' => self::estimateMuscleMass($profile, $measurements),
+                'fat_percentage_estimate' => self::estimateFatPercentage($profile, $measurements),
+                'bone_density_level' => self::estimateBoneDensity($profile->age, $profile->gender)
+            ];
+
+            // 身体比例分析
+            $ratios = $measurements->calculateBodyRatios();
+            $analysis['body_proportions'] = [
+                'waist_hip_ratio' => $ratios['waist_hip_ratio'] ?? 0,
+                'waist_hip_assessment' => self::assessWaistHipRatio($ratios['waist_hip_ratio'] ?? 0, $profile->gender),
+                'symmetry_score' => self::calculateSymmetryScore($measurements)
+            ];
+        }
+
+        // 体型分析
+        $typeAnalysis = [];
+        foreach ($bodyTypes as $selection) {
+            $typeAnalysis[$selection->type_category] = [
+                'selected_type' => $selection->typeConfig->type_name,
+                'suitability' => self::assessTypeSuitability($selection, $measurements)
+            ];
+        }
+        $analysis['body_types'] = $typeAnalysis;
+
+        // 风险评估
+        $analysis['risk_assessment'] = self::assessHealthRisks($profile, $measurements);
+
+        return $analysis;
+    }
+
+    /**
+     * 生成建议内容
+     */
+    private static function generateRecommendations($profile, $measurements, $analysis)
+    {
+        $recommendations = [
+            'exercise' => [],
+            'nutrition' => [],
+            'lifestyle' => [],
+            'clothing' => []
+        ];
+
+        // 运动建议
+        $bmi = $profile->calculateBMI();
+        if ($bmi < 18.5) {
+            $recommendations['exercise'][] = '增加力量训练,促进肌肉增长';
+            $recommendations['exercise'][] = '适量有氧运动,提高体能';
+        } elseif ($bmi > 24) {
+            $recommendations['exercise'][] = '增加有氧运动,控制体重';
+            $recommendations['exercise'][] = '结合力量训练,提高代谢';
+        } else {
+            $recommendations['exercise'][] = '保持规律运动,维持现有状态';
+        }
+
+        // 营养建议
+        if ($profile->age < 30) {
+            $recommendations['nutrition'][] = '保证充足蛋白质摄入,支持肌肉发育';
+        } elseif ($profile->age >= 50) {
+            $recommendations['nutrition'][] = '增加钙质摄入,预防骨质疏松';
+        }
+
+        // 生活方式建议
+        $recommendations['lifestyle'][] = '保持充足睡眠,每天7-8小时';
+        $recommendations['lifestyle'][] = '减少久坐时间,每小时活动一次';
+
+        // 服装搭配建议
+        $waistHipRatio = $analysis['body_proportions']['waist_hip_ratio'] ?? 0;
+        if ($waistHipRatio > 0.8 && $profile->gender == 2) {
+            $recommendations['clothing'][] = 'A字裙装修饰腰臀比例';
+            $recommendations['clothing'][] = '高腰设计拉长腿部线条';
+        }
+
+        return $recommendations;
+    }
+
+    /**
+     * 计算健康评分
+     */
+    private static function calculateHealthScore($profile, $measurements)
+    {
+        $score = 0;
+        $factors = 0;
+
+        // BMI评分 (30分)
+        $bmi = $profile->calculateBMI();
+        if ($bmi >= 18.5 && $bmi < 24) {
+            $score += 30;
+        } elseif ($bmi >= 17 && $bmi < 28) {
+            $score += 20;
+        } else {
+            $score += 10;
+        }
+        $factors++;
+
+        // 腰臀比评分 (20分)
+        if ($measurements && $measurements->waist > 0 && $measurements->hip > 0) {
+            $whr = $measurements->waist / $measurements->hip;
+            $idealWhr = $profile->gender == 1 ? 0.9 : 0.8;
+            
+            if (abs($whr - $idealWhr) <= 0.05) {
+                $score += 20;
+            } elseif (abs($whr - $idealWhr) <= 0.1) {
+                $score += 15;
+            } else {
+                $score += 10;
+            }
+            $factors++;
+        }
+
+        // 年龄调整评分 (10分)
+        if ($profile->age <= 30) {
+            $score += 10;
+        } elseif ($profile->age <= 50) {
+            $score += 8;
+        } else {
+            $score += 6;
+        }
+        $factors++;
+
+        return $factors > 0 ? round($score / $factors * 100 / 60, 1) : 0;
+    }
+
+    /**
+     * 计算身体年龄
+     */
+    private static function calculateBodyAge($profile, $measurements, $healthScore)
+    {
+        $baseAge = $profile->age;
+        
+        // 根据健康评分调整
+        if ($healthScore >= 90) {
+            $adjustment = -5;
+        } elseif ($healthScore >= 80) {
+            $adjustment = -2;
+        } elseif ($healthScore >= 70) {
+            $adjustment = 0;
+        } elseif ($healthScore >= 60) {
+            $adjustment = 3;
+        } else {
+            $adjustment = 8;
+        }
+
+        // 根据BMI调整
+        $bmi = $profile->calculateBMI();
+        if ($bmi < 18.5 || $bmi > 28) {
+            $adjustment += 2;
+        }
+
+        return max(18, min(80, $baseAge + $adjustment));
+    }
+
+    // 辅助方法
+    private static function estimateMuscleMass($profile, $measurements)
+    {
+        // 简化的肌肉量估算
+        return $profile->gender == 1 ? '中等' : '一般';
+    }
+
+    private static function estimateFatPercentage($profile, $measurements)
+    {
+        $bmi = $profile->calculateBMI();
+        if ($bmi < 18.5) return '偏低';
+        if ($bmi > 25) return '偏高';
+        return '正常';
+    }
+
+    private static function estimateBoneDensity($age, $gender)
+    {
+        if ($age < 30) return '良好';
+        if ($age > 50 && $gender == 2) return '需关注';
+        return '一般';
+    }
+
+    private static function assessWaistHipRatio($ratio, $gender)
+    {
+        if ($ratio == 0) return '无法评估';
+        
+        $ideal = $gender == 1 ? 0.9 : 0.8;
+        if (abs($ratio - $ideal) <= 0.05) return '理想';
+        if ($ratio > $ideal + 0.1) return '偏高';
+        return '一般';
+    }
+
+    private static function calculateSymmetryScore($measurements)
+    {
+        return mt_rand(75, 95); // 简化实现
+    }
+
+    private static function assessTypeSuitability($selection, $measurements)
+    {
+        return '适合'; // 简化实现
+    }
+
+    private static function assessHealthRisks($profile, $measurements)
+    {
+        $risks = [];
+        
+        $bmi = $profile->calculateBMI();
+        if ($bmi > 28) {
+            $risks[] = '肥胖相关疾病风险';
+        }
+        
+        if ($profile->age > 50) {
+            $risks[] = '骨质疏松风险';
+        }
+
+        return $risks;
+    }
+}

+ 160 - 0
application/common/model/BodyMeasurements.php

@@ -0,0 +1,160 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * 身体测量数据模型
+ */
+class BodyMeasurements extends Model
+{
+    // 表名
+    protected $table = 'fa_body_measurements';
+    
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'measurement_date_text'
+    ];
+
+    /**
+     * 关联身体档案
+     */
+    public function profile()
+    {
+        return $this->belongsTo('BodyProfile', 'profile_id', 'id', [], 'LEFT')->setEagerlyType(0);
+    }
+
+    /**
+     * 获取测量日期文本
+     */
+    public function getMeasurementDateTextAttr($value, $data)
+    {
+        return $data['measurement_date'] ? date('Y-m-d', $data['measurement_date']) : '';
+    }
+
+    /**
+     * 设置测量日期
+     */
+    public function setMeasurementDateAttr($value)
+    {
+        return is_numeric($value) ? $value : strtotime($value);
+    }
+
+    /**
+     * 获取所有测量数据的字段映射
+     */
+    public static function getMeasurementFields($gender = 0)
+    {
+        $commonFields = [
+            'chest' => '胸围',
+            'waist' => '腰围',
+            'hip' => '臀围',
+            'thigh' => '大腿围',
+            'calf' => '小腿围',
+            'upper_arm' => '上臂围',
+            'forearm' => '前臂围',
+            'neck' => '颈围',
+            'shoulder_width' => '肩宽',
+            'inseam' => '内缝长',
+            'outseam' => '外缝长',
+            'shoe_size' => '鞋码'
+        ];
+
+        // 女性专用字段
+        if ($gender == 2) {
+            $commonFields['bust'] = '胸围/乳围';
+            $commonFields['underbust'] = '下胸围';
+        }
+
+        return $commonFields;
+    }
+
+    /**
+     * 计算身体比例数据
+     */
+    public function calculateBodyRatios()
+    {
+        $profile = $this->profile;
+        if (!$profile || $profile->height <= 0) {
+            return [];
+        }
+
+        $height = $profile->height;
+        $ratios = [];
+
+        // 计算各部位与身高的比例
+        $measurementFields = [
+            'chest' => '胸围比例',
+            'waist' => '腰围比例', 
+            'hip' => '臀围比例',
+            'thigh' => '大腿围比例',
+            'shoulder_width' => '肩宽比例'
+        ];
+
+        foreach ($measurementFields as $field => $label) {
+            if ($this->$field > 0) {
+                $ratios[$field] = round(($this->$field / $height) * 100, 2);
+            }
+        }
+
+        // 计算腰臀比
+        if ($this->waist > 0 && $this->hip > 0) {
+            $ratios['waist_hip_ratio'] = round($this->waist / $this->hip, 2);
+        }
+
+        return $ratios;
+    }
+
+    /**
+     * 获取理想测量范围建议
+     */
+    public function getIdealRanges($gender, $height, $weight)
+    {
+        if ($height <= 0) return [];
+
+        $ranges = [];
+
+        // 根据身高和性别计算理想范围
+        if ($gender == 1) { // 男性
+            $ranges = [
+                'chest' => [
+                    'min' => round($height * 0.48, 1),
+                    'max' => round($height * 0.52, 1)
+                ],
+                'waist' => [
+                    'min' => round($height * 0.42, 1),
+                    'max' => round($height * 0.47, 1)
+                ],
+                'hip' => [
+                    'min' => round($height * 0.51, 1),
+                    'max' => round($height * 0.55, 1)
+                ]
+            ];
+        } else { // 女性
+            $ranges = [
+                'bust' => [
+                    'min' => round($height * 0.49, 1),
+                    'max' => round($height * 0.53, 1)
+                ],
+                'waist' => [
+                    'min' => round($height * 0.37, 1),
+                    'max' => round($height * 0.42, 1)
+                ],
+                'hip' => [
+                    'min' => round($height * 0.52, 1),
+                    'max' => round($height * 0.56, 1)
+                ]
+            ];
+        }
+
+        return $ranges;
+    }
+}

+ 182 - 0
application/common/model/BodyProfile.php

@@ -0,0 +1,182 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+use traits\model\SoftDelete;
+
+/**
+ * 身体档案模型
+ */
+class BodyProfile extends Model
+{
+    use SoftDelete;
+
+    // 表名
+    protected $table = 'fa_body_profile';
+    
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = 'deletetime';
+
+    // 追加属性
+    protected $append = [
+        'gender_text',
+        'body_photos_text',
+        'is_own_text'
+    ];
+
+    /**
+     * 关联用户
+     */
+    public function user()
+    {
+        return $this->belongsTo('User', 'user_id', 'id', [], 'LEFT')->setEagerlyType(0);
+    }
+
+    /**
+     * 关联身体测量数据
+     */
+    public function measurements()
+    {
+        return $this->hasMany('BodyMeasurements', 'profile_id', 'id');
+    }
+
+    /**
+     * 关联最新的身体测量数据
+     */
+    public function latestMeasurement()
+    {
+        return $this->hasOne('BodyMeasurements', 'profile_id', 'id')->order('measurement_date DESC');
+    }
+
+    /**
+     * 关联身体类型选择
+     */
+    public function bodyTypeSelections()
+    {
+        return $this->hasMany('BodyTypeSelection', 'profile_id', 'id');
+    }
+
+    /**
+     * 关联AI报告
+     */
+    public function aiReports()
+    {
+        return $this->hasMany('BodyAiReport', 'profile_id', 'id');
+    }
+
+    /**
+     * 关联最新AI报告
+     */
+    public function latestAiReport()
+    {
+        return $this->hasOne('BodyAiReport', 'profile_id', 'id')->order('generated_time DESC');
+    }
+
+    /**
+     * 获取性别文本
+     */
+    public function getGenderTextAttr($value, $data)
+    {
+        $genderMap = [
+            1 => '男',
+            2 => '女'
+        ];
+        return isset($genderMap[$data['gender']]) ? $genderMap[$data['gender']] : '未知';
+    }
+
+    /**
+     * 获取是否本人档案文本
+     */
+    public function getIsOwnTextAttr($value, $data)
+    {
+        return $data['is_own'] ? '本人档案' : '他人档案';
+    }
+
+    /**
+     * 获取身体照片数组
+     */
+    public function getBodyPhotosTextAttr($value, $data)
+    {
+        if (empty($data['body_photos'])) {
+            return [
+                'front' => '',
+                'side' => '',
+                'back' => ''
+            ];
+        }
+        return json_decode($data['body_photos'], true);
+    }
+
+    /**
+     * 设置身体照片
+     */
+    public function setBodyPhotosAttr($value)
+    {
+        return is_array($value) ? json_encode($value) : $value;
+    }
+
+    /**
+     * 获取完整的档案信息(包含测量数据和体型选择)
+     */
+    public function getFullProfileData()
+    {
+        $profile = $this->toArray();
+        
+        // 获取最新测量数据
+        $latestMeasurement = $this->latestMeasurement;
+        $profile['measurements'] = $latestMeasurement ? $latestMeasurement->toArray() : null;
+        
+        // 获取体型选择
+        $bodyTypes = $this->bodyTypeSelections()->with('typeConfig')->select();
+        $profile['body_types'] = [];
+        foreach ($bodyTypes as $selection) {
+            $profile['body_types'][$selection['type_category']] = [
+                'type_id' => $selection['selected_type_id'],
+                'type_name' => $selection->typeConfig ? $selection->typeConfig['type_name'] : '',
+                'type_image' => $selection->typeConfig ? $selection->typeConfig['type_image'] : ''
+            ];
+        }
+        
+        // 获取最新AI报告
+        $latestReport = $this->latestAiReport;
+        $profile['ai_report'] = $latestReport ? $latestReport->toArray() : null;
+        
+        return $profile;
+    }
+
+    /**
+     * 计算BMI指数
+     */
+    public function calculateBMI()
+    {
+        if ($this->height <= 0 || $this->weight <= 0) {
+            return 0;
+        }
+        
+        $heightInMeters = $this->height / 100;
+        return round($this->weight / ($heightInMeters * $heightInMeters), 2);
+    }
+
+    /**
+     * 获取BMI等级
+     */
+    public function getBMILevel()
+    {
+        $bmi = $this->calculateBMI();
+        
+        if ($bmi < 18.5) {
+            return '偏瘦';
+        } elseif ($bmi < 24) {
+            return '正常';
+        } elseif ($bmi < 28) {
+            return '超重';
+        } else {
+            return '肥胖';
+        }
+    }
+}

+ 163 - 0
application/common/model/BodyTypeConfig.php

@@ -0,0 +1,163 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * 身体类型配置模型
+ */
+class BodyTypeConfig extends Model
+{
+    // 表名
+    protected $table = 'fa_body_type_config';
+    
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    // 追加属性
+    protected $append = [
+        'gender_text',
+        'full_image_url'
+    ];
+
+    /**
+     * 获取性别文本
+     */
+    public function getGenderTextAttr($value, $data)
+    {
+        $genderMap = [
+            0 => '通用',
+            1 => '男性',
+            2 => '女性'
+        ];
+        return isset($genderMap[$data['gender']]) ? $genderMap[$data['gender']] : '未知';
+    }
+
+    /**
+     * 获取完整图片URL
+     */
+    public function getFullImageUrlAttr($value, $data)
+    {
+        if (empty($data['type_image'])) {
+            return '';
+        }
+        
+        // 如果已经是完整URL,直接返回
+        if (strpos($data['type_image'], 'http') === 0) {
+            return $data['type_image'];
+        }
+        
+        // 拼接完整URL
+        return request()->domain() . $data['type_image'];
+    }
+
+    /**
+     * 获取某个分类下适用于指定性别的所有体型
+     */
+    public static function getTypesByCategory($category, $gender = 0)
+    {
+        $query = self::where([
+            'type_category' => $category,
+            'status' => 1
+        ]);
+
+        if ($gender > 0) {
+            $query->where('gender', 'in', [0, $gender]);
+        }
+
+        return $query->order('sort ASC, id ASC')
+            ->select();
+    }
+
+    /**
+     * 获取所有体型分类
+     */
+    public static function getAllCategories($gender = 0)
+    {
+        $query = self::where('status', 1);
+        
+        if ($gender > 0) {
+            $query->where('gender', 'in', [0, $gender]);
+        }
+
+        $categories = $query->group('type_category')
+            ->column('type_category');
+
+        // 分类名称映射
+        $categoryNames = [
+            'shoulder' => '肩型',
+            'chest' => '胸型',
+            'bust' => '胸型',
+            'waist' => '腰型',
+            'hip' => '臀型',
+            'leg' => '腿型'
+        ];
+
+        $result = [];
+        foreach ($categories as $category) {
+            $result[$category] = [
+                'name' => isset($categoryNames[$category]) ? $categoryNames[$category] : $category,
+                'types' => self::getTypesByCategory($category, $gender),
+            ];
+        }
+
+        return $result;
+    }
+
+    /**
+     * 获取推荐的体型选择
+     * 基于身体测量数据推荐合适的体型
+     */
+    public static function getRecommendedTypes($measurements, $gender)
+    {
+        $recommendations = [];
+
+        if (!$measurements) {
+            return $recommendations;
+        }
+
+        // 肩型推荐逻辑
+        if ($measurements['shoulder_width'] > 0 && $measurements['chest'] > 0) {
+            $shoulderChestRatio = $measurements['shoulder_width'] / $measurements['chest'];
+            
+            if ($shoulderChestRatio > 0.55) {
+                $recommendations['shoulder'] = ['type_name' => '宽肩', 'confidence' => 0.8];
+            } elseif ($shoulderChestRatio < 0.45) {
+                $recommendations['shoulder'] = ['type_name' => '削肩', 'confidence' => 0.7];
+            } else {
+                $recommendations['shoulder'] = ['type_name' => '平肩', 'confidence' => 0.6];
+            }
+        }
+
+        // 腰型推荐逻辑
+        if ($measurements['waist'] > 0 && $measurements['hip'] > 0) {
+            $waistHipRatio = $measurements['waist'] / $measurements['hip'];
+            
+            if ($waistHipRatio < 0.7) {
+                $recommendations['waist'] = ['type_name' => '细腰', 'confidence' => 0.8];
+            } elseif ($waistHipRatio > 0.85) {
+                $recommendations['waist'] = ['type_name' => '圆腰', 'confidence' => 0.7];
+            } else {
+                $recommendations['waist'] = ['type_name' => '正常', 'confidence' => 0.6];
+            }
+        }
+
+        // 女性胸型推荐逻辑
+        if ($gender == 2 && isset($measurements['bust']) && $measurements['bust'] > 0) {
+            if ($measurements['bust'] < 80) {
+                $recommendations['bust'] = ['type_name' => '小巧', 'confidence' => 0.7];
+            } elseif ($measurements['bust'] > 95) {
+                $recommendations['bust'] = ['type_name' => '丰满', 'confidence' => 0.8];
+            } else {
+                $recommendations['bust'] = ['type_name' => '正常', 'confidence' => 0.6];
+            }
+        }
+
+        return $recommendations;
+    }
+}

+ 125 - 0
application/common/model/BodyTypeSelection.php

@@ -0,0 +1,125 @@
+<?php
+
+namespace app\common\model;
+
+use think\Model;
+
+/**
+ * 身体类型选择记录模型
+ */
+class BodyTypeSelection extends Model
+{
+    // 表名
+    protected $table = 'fa_body_type_selection';
+    
+    // 开启自动写入时间戳字段
+    protected $autoWriteTimestamp = 'int';
+    // 定义时间戳字段名
+    protected $createTime = 'createtime';
+    protected $updateTime = 'updatetime';
+    protected $deleteTime = false;
+
+    /**
+     * 关联身体档案
+     */
+    public function profile()
+    {
+        return $this->belongsTo('BodyProfile', 'profile_id', 'id', [], 'LEFT')->setEagerlyType(0);
+    }
+
+    /**
+     * 关联体型配置
+     */
+    public function typeConfig()
+    {
+        return $this->belongsTo('BodyTypeConfig', 'selected_type_id', 'id', [], 'LEFT')->setEagerlyType(0);
+    }
+
+    /**
+     * 批量保存用户的体型选择
+     */
+    public static function saveUserSelections($profileId, $selections)
+    {
+        if (empty($selections) || !is_array($selections)) {
+            return false;
+        }
+
+        // 开启事务
+        \think\Db::startTrans();
+        try {
+            foreach ($selections as $category => $typeId) {
+                // 检查是否已存在该分类的选择
+                $existing = self::where([
+                    'profile_id' => $profileId,
+                    'type_category' => $category
+                ])->find();
+
+                if ($existing) {
+                    // 更新现有记录
+                    $existing->selected_type_id = $typeId;
+                    $existing->save();
+                } else {
+                    // 创建新记录
+                    $selection = new self();
+                    $selection->profile_id = $profileId;
+                    $selection->type_category = $category;
+                    $selection->selected_type_id = $typeId;
+                    $selection->save();
+                }
+            }
+
+            \think\Db::commit();
+            return true;
+        } catch (\Exception $e) {
+            \think\Db::rollback();
+            return false;
+        }
+    }
+
+    /**
+     * 获取用户的所有体型选择
+     */
+    public static function getUserSelections($profileId)
+    {
+        $selections = self::where('profile_id', $profileId)
+            ->with('typeConfig')
+            ->select();
+
+        $result = [];
+        foreach ($selections as $selection) {
+            $result[$selection['type_category']] = [
+                'type_id' => $selection['selected_type_id'],
+                'type_name' => $selection->typeConfig ? $selection->typeConfig['type_name'] : '',
+                'type_image' => $selection->typeConfig ? $selection->typeConfig['type_image'] : '',
+                'description' => $selection->typeConfig ? $selection->typeConfig['description'] : ''
+            ];
+        }
+
+        return $result;
+    }
+
+    /**
+     * 删除档案的所有体型选择
+     */
+    public static function deleteByProfile($profileId)
+    {
+        return self::where('profile_id', $profileId)->delete();
+    }
+
+    /**
+     * 获取体型选择统计数据
+     */
+    public static function getSelectionStatistics($category = null)
+    {
+        $query = self::alias('s')
+            ->join('fa_body_type_config c', 's.selected_type_id = c.id')
+            ->field('c.type_name, COUNT(s.id) as count')
+            ->group('s.selected_type_id');
+
+        if ($category) {
+            $query->where('s.type_category', $category);
+        }
+
+        return $query->select()->toArray();
+    }
+}