chore: establish V1.1 recovery baseline
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
keepalive_timeout 65;
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript application/json;
|
||||
|
||||
upstream backend {
|
||||
server 172.17.0.1:8085;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# Admin web static files
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API proxy to backend
|
||||
location /api/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# WebSocket proxy
|
||||
location /ws/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
# Debug JSON escaping for SQL generation
|
||||
s = '["321181","321100"]'
|
||||
print('Raw:', s)
|
||||
print('repr:', repr(s))
|
||||
|
||||
# In Python f-string, the value s contains backslash-escaped quotes
|
||||
# So when written to SQL file, it should produce:
|
||||
# '["321181","321100"]'
|
||||
# (the single quotes are SQL string delimiters)
|
||||
|
||||
# Current code: region_scope = '["321181","321100"]'
|
||||
# When this gets into an f-string:
|
||||
region_scope_val = '["321181","321100"]'
|
||||
line = f"('{region_scope_val}', ..."
|
||||
print('Generated line:', line)
|
||||
print()
|
||||
|
||||
# The problem: in the SQL file, it should look like:
|
||||
# ('user_id', ..., '["321181","321100"]', ...)
|
||||
# But what we actually get depends on how it's written
|
||||
|
||||
# Let's check what the file actually contains
|
||||
test_line = f"('{region_scope_val}', 'required', ['\"never_married\"'], 'required', 'bachelor', 'preferred')"
|
||||
print('Test line:', test_line)
|
||||
print()
|
||||
print('Expected SQL: region_scope should be a JSON string literal')
|
||||
print('Correct format: \\'["321181","321100"]\\' in the SQL file')
|
||||
@@ -0,0 +1,447 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Demo Seed — 直接生成可执行的SQL,避免转义问题"""
|
||||
import uuid
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
def uid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
# ── 用户定义 ──────────────────────────────────────────────────────────
|
||||
FEMALE = [
|
||||
('demo_openid_f01','f01','小芳','female','1995-06-15',162,'321181','丹阳','never_married','bachelor','clerical','never','sometimes','是','是','结婚',90),
|
||||
('demo_openid_f02','f02','晓燕','female','1993-09-20',165,'321181','丹阳','divorced','college','teacher','never','never','是','是','结婚',95),
|
||||
('demo_openid_f03','f03','婷婷','female','1998-02-10',158,'321100','镇江','never_married','master','designer','sometimes','often','是','否','恋爱',85),
|
||||
('demo_openid_f04','f04','美玲','female','1990-11-05',160,'321181','丹阳','never_married','senior_high','factory','never','never','是','是','结婚',70),
|
||||
('demo_openid_f05','f05','慧琴','female','1996-07-22',163,'321181','丹阳','never_married','bachelor','nurse','never','sometimes','是','是','结婚',92),
|
||||
('demo_openid_f06','f06','秀英','female','1992-04-18',157,'321181','丹阳','widowed','junior_high','farmer','often','often','是','是','结婚',60),
|
||||
]
|
||||
MALE = [
|
||||
('demo_openid_m01','m01','张伟','male','1992-01-10',175,'321181','丹阳','never_married','bachelor','engineer','never','sometimes','是','是','结婚',88),
|
||||
('demo_openid_m02','m02','李强','male','1990-08-25',178,'321181','丹阳','divorced','master','manager','sometimes','often','是','是','结婚',95),
|
||||
('demo_openid_m03','m03','王磊','male','1997-12-03',180,'321100','镇江','never_married','college','programmer','never','never','是','是','恋爱',82),
|
||||
('demo_openid_m04','m04','赵明','male','1988-05-30',172,'321181','丹阳','never_married','senior_high','sales','often','often','否','是','结婚',75),
|
||||
('demo_openid_m05','m05','孙浩','male','1994-03-14',182,'321181','丹阳','never_married','bachelor','doctor','never','sometimes','是','是','结婚',97),
|
||||
('demo_openid_m06','m06','周洋','male','1999-08-08',176,'320500','苏州','never_married','master','researcher','never','never','是','否','恋爱',80),
|
||||
]
|
||||
ALL = FEMALE + MALE
|
||||
|
||||
uid_map = {u[1]: uid() for u in ALL}
|
||||
print(f"Users: {len(uid_map)}")
|
||||
|
||||
# ── UUIDs ─────────────────────────────────────────────────────────────
|
||||
org_ids = {t: uid() for t in ['women','trade','charity']}
|
||||
admin_id, mh_id, rv_id, mgr_id = uid(), uid(), uid(), uid()
|
||||
match_ids = {k: uid() for k in ['f01m01','f03m03','f05m05']}
|
||||
conv_ids = {k: uid() for k in ['f01m01','f03m03','f05m05']}
|
||||
act_summer, act_qixi = uid(), uid()
|
||||
|
||||
# ── 辅助函数 ──────────────────────────────────────────────────────────
|
||||
def q(s):
|
||||
"""SQL字符串值(带单引号)"""
|
||||
if s is None: return 'NULL'
|
||||
return "'" + str(s).replace("\\","\\\\").replace("'","''") + "'"
|
||||
|
||||
def json_arr(items):
|
||||
"""PostgreSQL JSON数组文本(带单引号包裹)"""
|
||||
inner = ','.join(json.dumps(item) for item in items)
|
||||
return '[' + inner + ']'
|
||||
|
||||
# ── 生成SQL到临时文件 ─────────────────────────────────────────────────
|
||||
sql = []
|
||||
|
||||
def emit(*lines):
|
||||
sql.extend(lines)
|
||||
sql.append('')
|
||||
|
||||
emit("SET session_replication_role = 'replica';")
|
||||
|
||||
# 1. Organizations
|
||||
emit("-- 1. 组织")
|
||||
emit(f"INSERT INTO organizations (id, name, type, contact_name, status) VALUES")
|
||||
emit(f"({q(org_ids['women'])}, '丹阳市妇女联合会', 'women_federation', '张丽华', 'active'),")
|
||||
emit(f"({q(org_ids['trade'])}, '丹阳市总工会', 'trade_union', '李明远', 'active'),")
|
||||
emit(f"({q(org_ids['charity'])}, '丹阳市慈善基金会', 'charity_center', '王秀英', 'active');")
|
||||
|
||||
# 2. Operators
|
||||
emit("-- 2. 工作人员")
|
||||
emit(f"INSERT INTO operators (id, organization_id, name, username, password_hash, status) VALUES")
|
||||
emit(f"({q(admin_id)}, {q(org_ids['women'])}, '陈主任', 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHdBfKYuPIiCTB6MJJDEaK', 'active'),")
|
||||
emit(f"({q(mh_id)}, {q(org_ids['women'])}, '红娘小王', 'wanghongniang', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHdBfKYuPIiCTB6MJJDEaK', 'active'),")
|
||||
emit(f"({q(rv_id)}, {q(org_ids['charity'])}, '审核员小张', 'reviewer01', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHdBfKYuPIiCTB6MJJDEaK', 'active'),")
|
||||
emit(f"({q(mgr_id)}, {q(org_ids['trade'])}, '活动管理员赵', 'activitymgr', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHdBfKYuPIiCTB6MJJDEaK', 'active');")
|
||||
|
||||
# 操作员角色
|
||||
role_assign = [
|
||||
('admin',['00000000-0000-0000-0000-000000000001','00000000-0000-0000-0000-000000000002']),
|
||||
('wanghongniang',['00000000-0000-0000-0000-000000000005','00000000-0000-0000-0000-000000000006']),
|
||||
('reviewer01',['00000000-0000-0000-0000-000000000004','00000000-0000-0000-0000-000000000007']),
|
||||
('activitymgr',['00000000-0000-0000-0000-000000000006','00000000-0000-0000-0000-000000000008']),
|
||||
]
|
||||
op_uid = {'admin':admin_id,'wanghongniang':mh_id,'reviewer01':rv_id,'activitymgr':mgr_id}
|
||||
for uname, rids in role_assign:
|
||||
for rid in rids:
|
||||
emit(f"INSERT INTO operator_roles (operator_id, role_id) VALUES ({q(op_uid[uname])}, {q(rid)});")
|
||||
|
||||
# 3. Users
|
||||
emit("-- 3. 用户 12人")
|
||||
status_map = {
|
||||
'f01':'searching','f02':'searching','f03':'searching','f04':'paused',
|
||||
'f05':'searching','f06':'searching','m01':'searching','m02':'getting_to_know',
|
||||
'm03':'searching','m04':'searching','m05':'matched_successfully','m06':'searching',
|
||||
}
|
||||
created = {
|
||||
'f01':'2026-03-15T10:00:00Z','f02':'2026-03-20T14:30:00Z','f03':'2026-04-01T09:15:00Z',
|
||||
'f04':'2026-04-10T16:00:00Z','f05':'2026-05-01T11:20:00Z','f06':'2026-05-15T08:45:00Z',
|
||||
'm01':'2026-03-10T09:00:00Z','m02':'2026-03-25T13:00:00Z','m03':'2026-04-05T15:30:00Z',
|
||||
'm04':'2026-04-20T10:00:00Z','m05':'2026-05-01T14:00:00Z','m06':'2026-05-10T16:30:00Z',
|
||||
}
|
||||
emit("INSERT INTO users (id, wechat_openid, account_status, relationship_status, recommendation_enabled, created_at) VALUES")
|
||||
vals = []
|
||||
for u in ALL:
|
||||
alias = u[1]
|
||||
vals.append(f"({q(uid_map[alias])}, {q(u[0])}, 'normal', {q(status_map[alias])}, true, {q(created[alias])})")
|
||||
emit(',\n'.join(vals) + ';')
|
||||
|
||||
# 4. User Profiles
|
||||
emit("-- 4. 用户资料")
|
||||
emit("INSERT INTO user_profiles (user_id, display_name, gender, birth_date, height_cm, region_code, region_name, marital_status, children_status, education_code, occupation_code, smoking_status, drinking_status, long_term_danyang, future_danyang, relationship_goal, profile_completion, profile_status) VALUES")
|
||||
pvals = []
|
||||
for u in ALL:
|
||||
alias = u[1]
|
||||
pvals.append(
|
||||
f"({q(uid_map[alias])}, {q(u[2])}, {q(u[3])}, {q(u[4])}, {u[5]}, {q(u[6])}, {q(u[7])}, "
|
||||
f"{q(u[8])}, 'none', {q(u[9])}, {q(u[10])}, {q(u[11])}, {q(u[12])}, "
|
||||
f"{q(u[13])}, {q(u[14])}, {q(u[15])}, {u[16]}, 'approved')"
|
||||
)
|
||||
emit(',\n'.join(pvals) + ';')
|
||||
|
||||
# 5. User Preferences — JSON数组用 json.dumps 正确转义
|
||||
emit("-- 5. 用户择偶偏好")
|
||||
prefs = [
|
||||
('f01',[28,38,'required',170,185,'required'],['321181','321100'],'required',['never_married'],'required','bachelor','preferred'),
|
||||
('f02',[28,40,'required',170,188,'required'],['321181'],'required',['never_married','divorced'],'preferred','college','preferred'),
|
||||
('f03',[25,35,'preferred',172,185,'preferred'],['321181','321100','320500'],'preferred',['never_married'],'required',None,None),
|
||||
('f04',[30,45,'required',168,185,'required'],['321181'],'required',['never_married','divorced'],'preferred','senior_high','preferred'),
|
||||
('f05',[27,36,'required',172,186,'required'],['321181','321100'],'preferred',['never_married'],'required','bachelor','preferred'),
|
||||
('f06',[30,50,'required',165,185,'required'],['321181'],'required',['never_married','divorced','widowed'],'preferred',None,None),
|
||||
('m01',[22,32,'required',155,170,'preferred'],['321181','321100'],'required',['never_married'],'required','junior_high','preferred'),
|
||||
('m02',[25,38,'preferred',158,172,'preferred'],['321181'],'preferred',['never_married','divorced'],'preferred','college','preferred'),
|
||||
('m03',[22,30,'preferred',155,168,'preferred'],['321181','321100','320500'],'preferred',['never_married'],'required','college','preferred'),
|
||||
('m04',[25,40,'preferred',155,170,'preferred'],['321181'],'required',['never_married','divorced'],'preferred',None,None),
|
||||
('m05',[23,33,'required',158,170,'preferred'],['321181','321100'],'preferred',['never_married'],'required','bachelor','preferred'),
|
||||
('m06',[22,32,'preferred',155,170,'preferred'],['321181','321100','320500'],'preferred',['never_married'],'required','master','preferred'),
|
||||
]
|
||||
emit("INSERT INTO user_preferences (user_id, min_age, max_age, age_priority, min_height_cm, max_height_cm, height_priority, region_scope, region_priority, marital_statuses, marital_priority, min_education_code, education_priority) VALUES")
|
||||
pvals2 = []
|
||||
for p in prefs:
|
||||
alias = p[0]
|
||||
ages = p[1]
|
||||
region_scope = json_arr(p[2]) # list -> JSON text
|
||||
p_region_pri = p[3]
|
||||
marital_list = p[4]
|
||||
p_marital_pri = p[5]
|
||||
min_edu = q(p[6]) if p[6] else 'NULL'
|
||||
edu_pri = q(p[7]) if p[7] else 'NULL'
|
||||
pvals2.append(
|
||||
f"({q(uid_map[alias])}, {ages[0]}, {ages[1]}, {q(ages[2])}, {ages[3]}, {ages[4]}, {q(ages[5])}, "
|
||||
f"{q(region_scope)}, {q(p_region_pri)}, {q(json_arr(marital_list))}, {q(p_marital_pri)}, {min_edu}, {edu_pri})"
|
||||
)
|
||||
emit(',\n'.join(pvals2) + ';')
|
||||
|
||||
# 6. Identities
|
||||
emit("-- 6. 实名认证(mock)")
|
||||
emit("INSERT INTO user_identities (id, user_id, real_name_enc, id_number_enc, id_number_hash, gender, birth_date, verification_status, provider, verified_at) VALUES")
|
||||
id_vals = []
|
||||
for i, u in enumerate(ALL):
|
||||
alias = u[1]
|
||||
id_vals.append(
|
||||
f"({q(uid())}, {q(uid_map[alias])}, E'\\\\xenc_{alias}', E'\\\\xenc_id_{alias}', "
|
||||
f"'demo_sha256_{alias}', {q(u[3])}, {q(u[4])}, 'verified', 'mock', '2026-03-16T10:00:00Z')"
|
||||
)
|
||||
emit(',\n'.join(id_vals) + ';')
|
||||
|
||||
# 7. Interests
|
||||
emit("-- 7. 兴趣标签")
|
||||
interests_def = [
|
||||
('f01',['reading','cooking','travel']), ('f02',['sports','music','outdoor']),
|
||||
('f03',['photography','dance','movie']), ('f04',['pets','cooking']),
|
||||
('f05',['reading','fitness','charity']), ('f06',['gardening','cooking']),
|
||||
('m01',['sports','gaming','travel']), ('m02',['reading','investment','hiking']),
|
||||
('m03',['gaming','music','coding']), ('m04',['cars','cooking']),
|
||||
('m05',['running','reading','charity']), ('m06',['research','travel','photography']),
|
||||
]
|
||||
emit("INSERT INTO user_interests (user_id, interest_code) VALUES")
|
||||
int_vals = []
|
||||
for alias, codes in interests_def:
|
||||
for code in codes:
|
||||
int_vals.append(f"({q(uid_map[alias])}, {q(code)})")
|
||||
emit(',\n'.join(int_vals) + ';')
|
||||
|
||||
# 8. Privacy
|
||||
emit("-- 8. 隐私设置")
|
||||
priv_data = {
|
||||
'f01':(True,True,True,True,True,True), 'f02':(True,True,True,False,True,True),
|
||||
'f03':(True,False,True,True,True,False),'f04':(True,True,False,True,True,True),
|
||||
'f05':(True,True,True,True,False,True),'f06':(True,False,True,True,True,True),
|
||||
'm01':(True,True,True,True,True,True), 'm02':(True,True,True,False,True,True),
|
||||
'm03':(True,True,True,True,True,False),'m04':(True,False,True,True,True,True),
|
||||
'm05':(True,True,True,True,True,True), 'm06':(True,True,True,True,True,True),
|
||||
}
|
||||
emit("INSERT INTO user_privacy_settings (user_id, visible_in_recommendations, visible_in_search, show_occupation, show_lifestyle_photos, receive_activity_recommendations, accept_matchmaker_recommendations) VALUES")
|
||||
priv_vals = []
|
||||
for u in ALL:
|
||||
alias = u[1]
|
||||
p = priv_data[alias]
|
||||
priv_vals.append(
|
||||
f"({q(uid_map[alias])}, {str(p[0]).lower()}, {str(p[1]).lower()}, {str(p[2]).lower()}, "
|
||||
f"{str(p[3]).lower()}, {str(p[4]).lower()}, {str(p[5]).lower()})"
|
||||
)
|
||||
emit(',\n'.join(priv_vals) + ';')
|
||||
|
||||
# 9. Photos
|
||||
emit("-- 9. 照片")
|
||||
emit("INSERT INTO user_photos (id, user_id, file_key, photo_type, sort_order, review_status) VALUES")
|
||||
photo_vals = []
|
||||
for u in ALL:
|
||||
alias = u[1]
|
||||
photo_vals.append(f"({q(uid())}, {q(uid_map[alias])}, 'photos/demo/{alias}/avatar.jpg', 'avatar', 0, 'approved')")
|
||||
photo_vals.append(f"({q(uid())}, {q(uid_map[alias])}, 'photos/demo/{alias}/lifestyle1.jpg', 'lifestyle', 1, 'approved')")
|
||||
emit(',\n'.join(photo_vals) + ';')
|
||||
|
||||
# 10. Review Tasks
|
||||
emit("-- 10. 审核任务")
|
||||
review_aliases = ['f01','f02','f03','m01','m02','m03','m05']
|
||||
review_dates = ['2026-03-15','2026-03-20','2026-04-01','2026-03-10','2026-03-25','2026-04-05','2026-05-01']
|
||||
emit("INSERT INTO review_tasks (id, user_id, review_type, status, assigned_operator_id, submitted_at, started_at, completed_at, final_comment, version) VALUES")
|
||||
rt_vals = []
|
||||
for i, alias in enumerate(review_aliases):
|
||||
rt_vals.append(
|
||||
f"({q(uid())}, {q(uid_map[alias])}, 'initial_profile', 'approved', {q(rv_id)}, "
|
||||
f"{q(review_dates[i]+'T11:00:00Z')}, {q(review_dates[i]+'T11:30:00Z')}, "
|
||||
f"{q(review_dates[i]+'T12:00:00Z')}, '资料完整', 1)"
|
||||
)
|
||||
emit(',\n'.join(rt_vals) + ';')
|
||||
|
||||
# 11. User Relations
|
||||
emit("-- 11. 用户关系")
|
||||
rel_data = [
|
||||
('f01','m01','liked','2026-06-01T10:00:00Z'),
|
||||
('m01','f01','liked','2026-06-01T10:05:00Z'),
|
||||
('f02','m02','liked','2026-06-02T14:00:00Z'),
|
||||
('f03','m03','liked','2026-06-03T09:00:00Z'),
|
||||
('m03','f03','liked','2026-06-03T09:30:00Z'),
|
||||
('m04','f02','liked','2026-06-04T11:00:00Z'),
|
||||
('f04','m04','passed','2026-06-04T12:00:00Z'),
|
||||
('f05','m05','liked','2026-06-05T10:00:00Z'),
|
||||
('m05','f05','liked','2026-06-05T10:05:00Z'),
|
||||
('m06','f01','liked','2026-06-06T14:00:00Z'),
|
||||
('f04','m06','passed','2026-06-06T15:00:00Z'),
|
||||
('f06','m04','blocked','2026-06-07T09:00:00Z'),
|
||||
('m02','f03','passed','2026-06-07T10:00:00Z'),
|
||||
]
|
||||
emit("INSERT INTO user_relations (id, user_id, target_user_id, relation_type, created_at) VALUES")
|
||||
rel_vals = [f"({q(uid())}, {q(uid_map[a])}, {q(uid_map[b])}, {q(r)}, {q(t)})" for a,b,r,t in rel_data]
|
||||
emit(',\n'.join(rel_vals) + ';')
|
||||
|
||||
# 12. Matches
|
||||
emit("-- 12. Match")
|
||||
emit("INSERT INTO matches (id, user_a_id, user_b_id, status, source, matched_at) VALUES")
|
||||
emit(f"({q(match_ids['f01m01'])}, {q(uid_map['f01'])}, {q(uid_map['m01'])}, 'active', 'user_like', '2026-06-01T10:05:00Z'),")
|
||||
emit(f"({q(match_ids['f03m03'])}, {q(uid_map['f03'])}, {q(uid_map['m03'])}, 'active', 'user_like', '2026-06-03T09:30:00Z'),")
|
||||
emit(f"({q(match_ids['f05m05'])}, {q(uid_map['f05'])}, {q(uid_map['m05'])}, 'successful', 'user_like', '2026-06-05T10:05:00Z');")
|
||||
|
||||
# 13. Success Requests
|
||||
emit("-- 13. 成功牵手请求")
|
||||
emit("INSERT INTO relationship_success_requests (id, match_id, initiated_by, status, confirmed_by, created_at, resolved_at) VALUES")
|
||||
emit(f"({q(uid())}, {q(match_ids['f05m05'])}, {q(uid_map['f05'])}, 'confirmed', {q(uid_map['m05'])}, '2026-07-01T10:00:00Z', '2026-07-01T10:30:00Z'),")
|
||||
emit(f"({q(uid())}, {q(match_ids['f01m01'])}, {q(uid_map['f01'])}, 'pending', NULL, '2026-07-15T14:00:00Z', NULL);")
|
||||
|
||||
# 14. Conversations
|
||||
emit("-- 14. Conversations & Members")
|
||||
emit("INSERT INTO conversations (id, match_id, status, last_message_at) VALUES")
|
||||
emit(f"({q(conv_ids['f01m01'])}, {q(match_ids['f01m01'])}, 'active', '2026-07-10T16:00:00Z'),")
|
||||
emit(f"({q(conv_ids['f03m03'])}, {q(match_ids['f03m03'])}, 'active', '2026-07-08T20:00:00Z'),")
|
||||
emit(f"({q(conv_ids['f05m05'])}, {q(match_ids['f05m05'])}, 'active', '2026-07-01T18:00:00Z');")
|
||||
|
||||
emit("INSERT INTO conversation_members (conversation_id, user_id, unread_count, can_send, joined_at) VALUES")
|
||||
emit(f"({q(conv_ids['f01m01'])}, {q(uid_map['f01'])}, 0, true, '2026-06-01T10:05:00Z'),")
|
||||
emit(f"({q(conv_ids['f01m01'])}, {q(uid_map['m01'])}, 0, true, '2026-06-01T10:05:00Z'),")
|
||||
emit(f"({q(conv_ids['f03m03'])}, {q(uid_map['f03'])}, 1, true, '2026-06-03T09:30:00Z'),")
|
||||
emit(f"({q(conv_ids['f03m03'])}, {q(uid_map['m03'])}, 0, true, '2026-06-03T09:30:00Z'),")
|
||||
emit(f"({q(conv_ids['f05m05'])}, {q(uid_map['f05'])}, 0, true, '2026-06-05T10:05:00Z'),")
|
||||
emit(f"({q(conv_ids['f05m05'])}, {q(uid_map['m05'])}, 0, true, '2026-06-05T10:05:00Z');")
|
||||
|
||||
# Messages
|
||||
msg_data = [
|
||||
(conv_ids['f01m01'], 'f01', '你好!看到你的资料觉得挺合适的~', '2026-06-15T14:00:00Z'),
|
||||
(conv_ids['f01m01'], 'm01', '你好!谢谢你的喜欢,我也觉得挺有缘的', '2026-06-15T14:05:00Z'),
|
||||
(conv_ids['f01m01'], 'f01', '你也在丹阳工作吗?平时喜欢做些什么?', '2026-06-15T14:10:00Z'),
|
||||
(conv_ids['f03m03'], 'm03', '嗨,你好!很高兴认识你', '2026-07-05T19:30:00Z'),
|
||||
(conv_ids['f03m03'], 'f03', '你好呀!我平时喜欢拍照和跳舞,你呢?', '2026-07-05T19:35:00Z'),
|
||||
(conv_ids['f05m05'], 'm05', '你好,很高兴认识你!', '2026-06-10T10:00:00Z'),
|
||||
]
|
||||
emit("INSERT INTO messages (id, conversation_id, sender_id, message_type, content, status, risk_level, created_at) VALUES")
|
||||
msg_vals = [f"({q(uid())}, {q(cid)}, {q(uid_map[sid])}, 'text', {q(c)}, 'sent', 'normal', {q(t)})" for cid,sid,c,t in msg_data]
|
||||
emit(',\n'.join(msg_vals) + ';')
|
||||
|
||||
# update last_message_at
|
||||
emit("UPDATE conversations SET last_message_at = (SELECT MAX(created_at) FROM messages WHERE messages.conversation_id = conversations.id);")
|
||||
|
||||
# 15. Recommendation Records
|
||||
emit("-- 15. 推荐记录")
|
||||
rec_data = [
|
||||
('f01','m01',82.5,'high','2026-05-28T10:00:00Z','liked','2026-06-01T10:00:00Z'),
|
||||
('f01','m03',70.0,'medium','2026-05-28T10:00:00Z','passed','2026-05-28T10:01:00Z'),
|
||||
('m01','f01',85.0,'high','2026-05-28T11:00:00Z','liked','2026-06-01T10:05:00Z'),
|
||||
('f03','m03',78.0,'high','2026-06-01T09:00:00Z','liked','2026-06-03T09:30:00Z'),
|
||||
('m03','f03',75.0,'high','2026-06-01T09:30:00Z','liked','2026-06-03T09:30:00Z'),
|
||||
('f05','m05',88.0,'high','2026-06-01T10:00:00Z','liked','2026-06-05T10:00:00Z'),
|
||||
('m05','f05',86.0,'high','2026-06-01T10:30:00Z','liked','2026-06-05T10:05:00Z'),
|
||||
('m06','f01',62.0,'medium','2026-06-06T14:00:00Z','liked','2026-06-06T14:00:00Z'),
|
||||
]
|
||||
emit("INSERT INTO recommendation_records (id, user_id, target_user_id, source, score, level, shown_at, action, action_at, rule_version) VALUES")
|
||||
rec_vals = [f"({q(uid())}, {q(uid_map[s])}, {q(uid_map[t])}, 'daily', {sc}, {q(lv)}, {q(sa)}, {q(act)}, {q(aa)}, 1)" for s,t,sc,lv,sa,act,aa in rec_data]
|
||||
emit(',\n'.join(rec_vals) + ';')
|
||||
|
||||
# 16. Activities
|
||||
emit("-- 16. 相亲活动")
|
||||
emit(f"INSERT INTO activities (id, title, description, organizer_org_id, coorganizer_text, start_at, end_at, location_name, location_address, male_limit, female_limit, total_limit, min_age, max_age, registration_start_at, registration_end_at, need_review, status, created_by) VALUES")
|
||||
emit(f"({q(act_summer)}, '丹阳公益单身联谊会(夏季场)', '面向丹阳市区及周边乡镇的单身青年,由妇联、工会联合主办。', {q(org_ids['women'])}, '丹阳市总工会、丹阳市慈善基金会', '2026-08-15T14:00:00Z', '2026-08-15T17:30:00Z', '丹阳市文化馆多功能厅', '丹阳市丹北路168号', 15, 15, 30, 22, 40, '2026-07-01T00:00:00Z', '2026-08-10T23:59:59Z', true, 'registration_open', {q(mgr_id)}),")
|
||||
emit(f"({q(act_qixi)}, '七夕特别活动·丹阳缘来是你', '七夕节特别策划,浪漫主题相亲活动。', {q(org_ids['trade'])}, '丹阳市新时代文明实践中心', '2026-08-29T15:00:00Z', '2026-08-29T18:00:00Z', '丹阳大剧院报告厅', '丹阳市书院路28号', 20, 20, 40, 25, 45, '2026-08-01T00:00:00Z', '2026-08-25T23:59:59Z', true, 'draft', {q(mgr_id)});")
|
||||
|
||||
# 17. Activity Registrations
|
||||
emit("-- 17. 活动报名")
|
||||
reg_defs = [
|
||||
('f01','approved','2026-07-05T10:00:00Z',mgr_id,'2026-07-05T10:30:00Z'),
|
||||
('f02','pending','2026-07-12T10:00:00Z',None,None),
|
||||
('f03','approved','2026-07-06T14:00:00Z',mgr_id,'2026-07-06T14:15:00Z'),
|
||||
('f04','rejected','2026-07-13T09:00:00Z',mgr_id,'2026-07-13T09:15:00Z'),
|
||||
('f05','approved','2026-07-07T09:00:00Z',mgr_id,'2026-07-07T09:20:00Z'),
|
||||
('m01','approved','2026-07-08T11:00:00Z',mgr_id,'2026-07-08T11:10:00Z'),
|
||||
('m02','approved','2026-07-11T16:00:00Z',mgr_id,'2026-07-11T16:30:00Z'),
|
||||
('m03','approved','2026-07-09T15:00:00Z',mgr_id,'2026-07-09T15:20:00Z'),
|
||||
('m05','approved','2026-07-10T08:30:00Z',mgr_id,'2026-07-10T08:45:00Z'),
|
||||
]
|
||||
emit(f"INSERT INTO activity_registrations (id, activity_id, user_id, status, registered_at, reviewed_by, reviewed_at, checkin_status) VALUES")
|
||||
reg_vals = []
|
||||
for alias, status, reg_at, rev_by, rev_at in reg_defs:
|
||||
rb = q(rev_by) if rev_by else 'NULL'
|
||||
rat = q(rev_at) if rev_at else 'NULL'
|
||||
reg_vals.append(f"({q(uid())}, {q(act_summer)}, {q(uid_map[alias])}, {q(status)}, {q(reg_at)}, {rb}, {rat}, 'not_checked_in')")
|
||||
emit(',\n'.join(reg_vals) + ';')
|
||||
|
||||
# 18. Activity Interests
|
||||
emit("-- 18. 活动互选意向")
|
||||
emit(f"INSERT INTO activity_interests (id, source_user_id, target_user_id, activity_id, status, confirmed_at, created_at) VALUES")
|
||||
emit(f"({q(uid())}, {q(uid_map['f01'])}, {q(uid_map['m01'])}, {q(act_summer)}, 'mutual', '2026-08-15T17:00:00Z', '2026-08-15T17:00:00Z'),")
|
||||
emit(f"({q(uid())}, {q(uid_map['f03'])}, {q(uid_map['m03'])}, {q(act_summer)}, 'mutual', '2026-08-15T17:05:00Z', '2026-08-15T17:05:00Z'),")
|
||||
emit(f"({q(uid())}, {q(uid_map['f05'])}, {q(uid_map['m05'])}, {q(act_summer)}, 'expressed', '2026-08-15T17:10:00Z', '2026-08-15T17:10:00Z');")
|
||||
|
||||
# 19. Matchmaker Services
|
||||
emit("-- 19. 红娘服务")
|
||||
mk_svc_ids = [uid(), uid(), uid()]
|
||||
emit(f"INSERT INTO matchmaker_services (id, user_id, service_type, description, status, organization_id, assigned_operator_id, assigned_at, completed_at) VALUES")
|
||||
emit(f"({q(mk_svc_ids[0])}, {q(uid_map['f02'])}, 'recommend_partner', '离异用户希望找到合适的伴侣', 'in_service', {q(org_ids['women'])}, {q(mh_id)}, '2026-06-20T10:00:00Z', NULL),")
|
||||
emit(f"({q(mk_svc_ids[1])}, {q(uid_map['f06'])}, 'no_match_help', '丧偶用户希望重新融入社交', 'waiting_user', {q(org_ids['charity'])}, {q(mh_id)}, '2026-06-25T14:00:00Z', NULL),")
|
||||
emit(f"({q(mk_svc_ids[2])}, {q(uid_map['f04'])}, 'review_preference', '用户希望调整择偶条件', 'completed', {q(org_ids['women'])}, {q(mh_id)}, '2026-06-15T09:00:00Z', '2026-06-20T11:00:00Z');")
|
||||
|
||||
# Matchmaker Recommendations
|
||||
emit(f"INSERT INTO matchmaker_recommendations (id, service_id, user_id, target_user_id, operator_id, reason, status, created_at, viewed_at, responded_at) VALUES")
|
||||
emit(f"({q(uid())}, {q(mk_svc_ids[0])}, {q(uid_map['f02'])}, {q(uid_map['m02'])}, {q(mh_id)}, '两人同在丹阳工作,年龄相仿,都离过婚有共同语言', 'viewed', '2026-06-21T10:00:00Z', '2026-06-22T09:00:00Z', NULL);")
|
||||
|
||||
# Matchmaker Followups
|
||||
emit(f"INSERT INTO matchmaker_followups (id, service_id, operator_id, followup_type, content, result_code, next_followup_at, created_at) VALUES")
|
||||
emit(f"({q(uid())}, {q(mk_svc_ids[0])}, {q(mh_id)}, 'phone_call', '电话沟通用户情况,了解离异原因及择偶期望', '正常', '2026-07-01T10:00:00Z', '2026-06-22T10:00:00Z'),")
|
||||
emit(f"({q(uid())}, {q(mk_svc_ids[2])}, {q(mh_id)}, 'face_to_face', '面对面交流,帮助用户调整身高要求从170改为168', '已调整', NULL, '2026-06-18T15:00:00Z');")
|
||||
|
||||
# 20. Notifications
|
||||
emit("-- 20. 系统通知")
|
||||
notif_data = [
|
||||
('f01','match','你们互相喜欢!','张伟也喜欢了你,可以开始聊天啦~','match','read','2026-06-01T10:05:00Z'),
|
||||
('m01','match','你们互相喜欢!','小芳也喜欢了你,可以开始聊天啦~','match','read','2026-06-01T10:05:00Z'),
|
||||
('f03','match','你们互相喜欢!','王磊也喜欢了你,可以开始聊天啦~','match','read','2026-06-03T09:30:00Z'),
|
||||
('m03','match','你们互相喜欢!','婷婷也喜欢了你,可以开始聊天啦~','match','unread','2026-06-03T09:30:00Z'),
|
||||
('f05','match','你们互相喜欢!','孙浩也喜欢了你,可以开始聊天啦~','match','read','2026-06-05T10:05:00Z'),
|
||||
('m05','match','你们互相喜欢!','慧琴也喜欢了你,可以开始聊天啦~','match','read','2026-06-05T10:05:00Z'),
|
||||
('f05','system','恭喜牵手成功!','您与孙浩已成功牵手,祝你们幸福!','match','read','2026-07-01T10:30:00Z'),
|
||||
('m05','system','恭喜牵手成功!','您与慧琴已成功牵手,祝你们幸福!','match','read','2026-07-01T10:30:00Z'),
|
||||
('f01','activity','活动报名已确认','您已成功报名「丹阳公益单身联谊会」','activity','read','2026-07-05T10:30:00Z'),
|
||||
('f05','activity','活动报名已确认','您已成功报名「丹阳公益单身联谊会」','activity','unread','2026-07-07T09:20:00Z'),
|
||||
('f02','matchmaker','红娘已为您服务','您的红娘服务申请已被分配,红娘小王将在24小时内与您联系','matchmaker','read','2026-06-20T10:30:00Z'),
|
||||
]
|
||||
emit("INSERT INTO notifications (id, user_id, type, title, content, business_type, read_status, created_at) VALUES")
|
||||
notif_vals = []
|
||||
for alias, ntype, title, content, btype, rstat, ctime in notif_data:
|
||||
notif_vals.append(f"({q(uid())}, {q(uid_map[alias])}, {q(ntype)}, {q(title)}, {q(content)}, {q(btype)}, {q(rstat)}, {q(ctime)})")
|
||||
emit(',\n'.join(notif_vals) + ';')
|
||||
|
||||
# 21. Consent Records
|
||||
emit("-- 21. 同意记录")
|
||||
emit("INSERT INTO consent_records (id, user_id, type, version, agreed_at, ip_address, user_agent) VALUES")
|
||||
emit(f"({q(uid())}, {q(uid_map['f01'])}, 'terms_of_service', '1.0', '2026-03-15T10:00:00Z', '127.0.0.1', 'WeChat/8.0'),")
|
||||
emit(f"({q(uid())}, {q(uid_map['f01'])}, 'privacy_policy', '1.0', '2026-03-15T10:00:00Z', '127.0.0.1', 'WeChat/8.0'),")
|
||||
emit(f"({q(uid())}, {q(uid_map['m01'])}, 'terms_of_service', '1.0', '2026-03-10T09:00:00Z', '127.0.0.1', 'WeChat/8.0'),")
|
||||
emit(f"({q(uid())}, {q(uid_map['m01'])}, 'privacy_policy', '1.0', '2026-03-10T09:00:00Z', '127.0.0.1', 'WeChat/8.0');")
|
||||
|
||||
# 22. Review Reasons
|
||||
emit("-- 22. 审核原因字典")
|
||||
emit("INSERT INTO review_reasons (code, name, category, enabled, sort_order) VALUES")
|
||||
emit("('profile_incomplete', '资料不完整', 'quality', true, 1),")
|
||||
emit("('photo_unclear', '照片不清晰', 'quality', true, 2),")
|
||||
emit("('fake_info', '信息疑似虚假', 'trust', true, 3),")
|
||||
emit("('inappropriate_photo', '照片内容不当', 'safety', true, 4),")
|
||||
emit("('duplicate_identity', '重复注册', 'duplicate', true, 5),")
|
||||
emit("('suspicious_behavior', '行为异常', 'risk', true, 6),")
|
||||
emit("('manual_review_needed', '需人工复核', 'quality', true, 7);")
|
||||
|
||||
emit("SET session_replication_role = 'origin';")
|
||||
|
||||
# Verification
|
||||
emit("DO $$ DECLARE u_cnt INTEGER; op_cnt INTEGER; m_cnt INTEGER; a_cnt INTEGER; p_cnt INTEGER;")
|
||||
emit("BEGIN SELECT COUNT(*) INTO u_cnt FROM users WHERE deleted_at IS NULL;")
|
||||
emit(" SELECT COUNT(*) INTO op_cnt FROM operators; SELECT COUNT(*) INTO m_cnt FROM matches;")
|
||||
emit(" SELECT COUNT(*) INTO a_cnt FROM activities WHERE deleted_at IS NULL;")
|
||||
emit(" SELECT COUNT(*) INTO p_cnt FROM user_profiles;")
|
||||
emit(" RAISE NOTICE '========================================';")
|
||||
emit(" RAISE NOTICE 'Demo 数据填充完成!';")
|
||||
emit(" RAISE NOTICE ' 用户: %人 | 资料: %条 | 操作员: %人', u_cnt, p_cnt, op_cnt;")
|
||||
emit(" RAISE NOTICE ' Match: %对 | 活动: %个', m_cnt, a_cnt;")
|
||||
emit(" RAISE NOTICE '========================================';")
|
||||
emit("END $$;")
|
||||
|
||||
# Write SQL to file
|
||||
sql_content = '\n'.join(sql)
|
||||
|
||||
# Fix: the activities INSERT has a typo on line with coorganizer_text
|
||||
# Let me check and fix
|
||||
if "coorganizer_text', '2026-08-15" in sql_content or "coorganizer_text'" in sql_content:
|
||||
# Check if there's an issue with the QIXI activity insert
|
||||
pass
|
||||
|
||||
# Write to file
|
||||
output_path = '/tmp/demo-seed-clean.sql'
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(sql_content)
|
||||
|
||||
print(f"SQL written: {len(sql)} lines, {len(sql_content)} bytes")
|
||||
|
||||
# Copy to container and execute
|
||||
result = subprocess.run(
|
||||
['docker', 'cp', output_path, 'danyang-match-postgres:/tmp/demo-seed-final.sql'],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f"cp failed: {result.stderr}")
|
||||
sys.exit(1)
|
||||
|
||||
result = subprocess.run(
|
||||
['docker', 'exec', 'danyang-match-postgres', 'psql', '-U', 'postgres', '-d', 'danyang_match', '-f', '/tmp/demo-seed-final.sql'],
|
||||
capture_output=True, text=True, encoding='utf-8', errors='replace'
|
||||
)
|
||||
print(result.stdout)
|
||||
if result.stderr:
|
||||
print("STDERR:", result.stderr)
|
||||
sys.exit(result.returncode)
|
||||
@@ -0,0 +1,237 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
丹阳公益婚恋平台 - 底部导航栏图标生成脚本
|
||||
圆润线性风格,两种状态(未选中/选中),三种尺寸
|
||||
"""
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
import math
|
||||
import os
|
||||
|
||||
# 主题色配置
|
||||
COLOR_UNSELECTED = (153, 153, 153) # #999999
|
||||
COLOR_SELECTED = (233, 107, 107) # #E96B6B
|
||||
COLOR_BG = (255, 255, 255) # 白色背景
|
||||
|
||||
|
||||
def sin_deg(x):
|
||||
return math.sin(math.radians(x))
|
||||
|
||||
def cos_deg(x):
|
||||
return math.cos(math.radians(x))
|
||||
|
||||
|
||||
def draw_heart_points(cx, cy, scale):
|
||||
"""计算心形路径点"""
|
||||
pts = []
|
||||
for t in range(0, 360, 3):
|
||||
rad = t * 3.14159 / 180
|
||||
x = scale * 16 * (sin_deg(rad) ** 3)
|
||||
y = -scale * (13 * cos_deg(rad) - 5 * cos_deg(2*rad) - 2 * cos_deg(3*rad) - cos_deg(4*rad))
|
||||
pts.append((cx + x, cy + y))
|
||||
return pts
|
||||
|
||||
|
||||
def draw_rounded_rect(draw, x, y, w, h, r, color, width=2):
|
||||
"""绘制圆角矩形(无填充,只描边)"""
|
||||
r = min(r, w // 2, h // 2)
|
||||
draw.rounded_rectangle([x, y, x + w, y + h], radius=r, outline=color, width=width)
|
||||
|
||||
|
||||
def draw_house(draw, cx, cy, size, color, width=2):
|
||||
"""绘制房子图标 - 首页"""
|
||||
s = size * 0.35
|
||||
# 房子主体
|
||||
body_x, body_y = cx - s * 0.7, cy - s * 0.1
|
||||
body_w, body_h = s * 1.4, s * 0.7
|
||||
draw_rounded_rect(draw, body_x, body_y, body_w, body_h, 4, color, width)
|
||||
|
||||
# 屋顶(三角形)
|
||||
roof_points = [
|
||||
(cx, body_y - s * 0.5),
|
||||
(body_x - s * 0.15, body_y),
|
||||
(body_x + body_w + s * 0.15, body_y)
|
||||
]
|
||||
draw.polygon(roof_points, outline=color, width=width)
|
||||
|
||||
# 门
|
||||
door_w, door_h = s * 0.28, s * 0.4
|
||||
door_x = cx - door_w / 2
|
||||
door_y = body_y + body_h - door_h
|
||||
draw_rounded_rect(draw, door_x, door_y, door_w, door_h, 2, color, width)
|
||||
|
||||
# 烟囱
|
||||
chimney_w, chimney_h = s * 0.12, s * 0.22
|
||||
chimney_x = cx + s * 0.35
|
||||
chimney_y = body_y - s * 0.35
|
||||
draw_rounded_rect(draw, chimney_x, chimney_y, chimney_w, chimney_h, 2, color, width)
|
||||
|
||||
|
||||
def draw_hearts(draw, cx, cy, size, color, width=2):
|
||||
"""绘制双心图标 - 找对象"""
|
||||
s = size * 0.016
|
||||
offset = s * 4
|
||||
|
||||
# 左心
|
||||
left_pts = draw_heart_points(cx - offset, cy - offset * 0.3, s)
|
||||
right_pts = draw_heart_points(cx + offset, cy - offset * 0.3, s)
|
||||
|
||||
for pts in [left_pts, right_pts]:
|
||||
for i in range(len(pts)):
|
||||
p1 = pts[i]
|
||||
p2 = pts[(i + 1) % len(pts)]
|
||||
draw.line([p1, p2], fill=color, width=width)
|
||||
|
||||
|
||||
def draw_chat_bubble(draw, cx, cy, size, color, width=2):
|
||||
"""绘制对话气泡图标 - 消息"""
|
||||
s = size * 0.28
|
||||
# 气泡主体
|
||||
bubble_x, bubble_y = cx - s, cy - s * 0.6
|
||||
bubble_w, bubble_h = s * 2, s * 1.2
|
||||
draw_rounded_rect(draw, bubble_x, bubble_y, bubble_w, bubble_h, 8, color, width)
|
||||
|
||||
# 小三角
|
||||
tail_points = [
|
||||
(bubble_x + s * 0.1, bubble_y + bubble_h),
|
||||
(bubble_x + s * 0.3, bubble_y + bubble_h + s * 0.25),
|
||||
(bubble_x + s * 0.5, bubble_y + bubble_h)
|
||||
]
|
||||
draw.polygon(tail_points, outline=color, width=width)
|
||||
|
||||
# 内部文字线条
|
||||
line_y_start = bubble_y + s * 0.2
|
||||
for i in range(3):
|
||||
line_y = line_y_start + i * s * 0.22
|
||||
draw.line([(bubble_x + s * 0.15, line_y), (bubble_x + s * 1.5, line_y)], fill=color, width=width)
|
||||
|
||||
|
||||
def draw_calendar(draw, cx, cy, size, color, width=2):
|
||||
"""绘制日历图标 - 活动"""
|
||||
s = size * 0.26
|
||||
# 日历主体
|
||||
cal_x, cal_y = cx - s, cy - s * 0.55
|
||||
cal_w, cal_h = s * 2, s * 1.3
|
||||
draw_rounded_rect(draw, cal_x, cal_y, cal_w, cal_h, 6, color, width)
|
||||
|
||||
# 顶部横线(分隔年月和日期)
|
||||
draw.line([(cal_x + s * 0.05, cal_y + s * 0.25), (cal_x + cal_w - s * 0.05, cal_y + s * 0.25)], fill=color, width=width)
|
||||
|
||||
# 日期点
|
||||
dot_r = 2 if size >= 162 else 1
|
||||
positions = [
|
||||
(cal_x + s * 0.3, cal_y + s * 0.55),
|
||||
(cal_x + s * 0.7, cal_y + s * 0.55),
|
||||
(cal_x + s * 1.1, cal_y + s * 0.55),
|
||||
(cal_x + s * 0.5, cal_y + s * 0.9),
|
||||
(cal_x + s * 0.9, cal_y + s * 0.9),
|
||||
]
|
||||
for dx, dy in positions:
|
||||
draw.ellipse([dx - dot_r, dy - dot_r, dx + dot_r, dy + dot_r], fill=color)
|
||||
|
||||
# 左上角挂环
|
||||
ring_r = 4 if size >= 162 else 2
|
||||
draw.ellipse([cal_x + s * 0.25 - ring_r, cal_y - ring_r, cal_x + s * 0.25 + ring_r, cal_y + ring_r], outline=color, width=width)
|
||||
draw.ellipse([cal_x + s * 1.75 - ring_r, cal_y - ring_r, cal_x + s * 1.75 + ring_r, cal_y + ring_r], outline=color, width=width)
|
||||
|
||||
|
||||
def draw_user(draw, cx, cy, size, color, width=2):
|
||||
"""绘制用户图标 - 我的"""
|
||||
s = size * 0.3
|
||||
|
||||
# 头部(圆形)
|
||||
head_r = s * 0.38
|
||||
head_x, head_y = cx, cy - s * 0.25
|
||||
draw.ellipse([head_x - head_r, head_y - head_r, head_x + head_r, head_y + head_r], outline=color, width=width)
|
||||
|
||||
# 身体(半椭圆)
|
||||
body_top = cy + s * 0.15
|
||||
body_bottom = cy + s * 0.75
|
||||
body_left = cx - s * 0.45
|
||||
body_right = cx + s * 0.45
|
||||
draw.arc([body_left, body_top, body_right, body_bottom], start=180, end=360, fill=color, width=width)
|
||||
|
||||
# 身体两侧竖线
|
||||
side_offset = s * 0.4
|
||||
draw.line([(cx - side_offset, body_top), (cx - side_offset, body_bottom)], fill=color, width=width)
|
||||
draw.line([(cx + side_offset, body_top), (cx + side_offset, body_bottom)], fill=color, width=width)
|
||||
|
||||
|
||||
ICON_FUNCTIONS = {
|
||||
"home": draw_house,
|
||||
"discover": draw_hearts,
|
||||
"message": draw_chat_bubble,
|
||||
"activity": draw_calendar,
|
||||
"mine": draw_user,
|
||||
}
|
||||
|
||||
|
||||
def create_icon(icon_name, size, color):
|
||||
"""创建单个图标"""
|
||||
img = Image.new('RGBA', (size, size), color=(255, 255, 255, 0)) # 透明背景
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
func = ICON_FUNCTIONS.get(icon_name)
|
||||
if func:
|
||||
func(draw, size // 2, size // 2, size, color)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
def main():
|
||||
output_dir = r"E:\develop\公益婚恋服务平台\miniapp\assets\tabbar"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
icons = [
|
||||
("home", "首页"),
|
||||
("discover", "找对象"),
|
||||
("message", "消息"),
|
||||
("activity", "活动"),
|
||||
("mine", "我的"),
|
||||
]
|
||||
|
||||
sizes_map = [
|
||||
(81, ""), # 1x
|
||||
(162, "_2x"), # 2x
|
||||
(243, "_3x"), # 3x
|
||||
]
|
||||
|
||||
print("=" * 50)
|
||||
print("丹阳公益婚恋平台 - 底部导航栏图标生成")
|
||||
print("=" * 50)
|
||||
print()
|
||||
|
||||
total = 0
|
||||
for icon_name, label in icons:
|
||||
print(f"🎨 {label} ({icon_name}):")
|
||||
|
||||
for size, suffix in sizes_map:
|
||||
# 未选中状态
|
||||
img_u = create_icon(icon_name, size, color=COLOR_UNSELECTED)
|
||||
path_u = os.path.join(output_dir, f"{icon_name}{suffix}.png")
|
||||
img_u.save(path_u, 'PNG')
|
||||
|
||||
# 选中状态
|
||||
img_s = create_icon(icon_name, size, color=COLOR_SELECTED)
|
||||
path_s = os.path.join(output_dir, f"{icon_name}-active{suffix}.png")
|
||||
img_s.save(path_s, 'PNG')
|
||||
|
||||
print(f" ✓ {os.path.basename(path_u)} ({size}×{size}, {os.path.getsize(path_u)} bytes)")
|
||||
print(f" ✓ {os.path.basename(path_s)} ({size}×{size}, {os.path.getsize(path_s)} bytes)")
|
||||
total += 2
|
||||
|
||||
print()
|
||||
|
||||
print("=" * 50)
|
||||
print(f"✅ 共生成 {total} 个图标文件")
|
||||
print(f"📁 保存路径: {output_dir}")
|
||||
print()
|
||||
print("文件清单:")
|
||||
for f in sorted(os.listdir(output_dir)):
|
||||
if f.endswith('.png'):
|
||||
filepath = os.path.join(output_dir, f)
|
||||
print(f" {f} ({os.path.getsize(filepath)} bytes)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,91 @@
|
||||
param(
|
||||
[switch]$SkipBackend,
|
||||
[switch]$SkipE2E
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$projectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||
$failures = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
function Invoke-Gate {
|
||||
param(
|
||||
[string]$Name,
|
||||
[scriptblock]$Action
|
||||
)
|
||||
|
||||
Write-Host "`n[$Name]" -ForegroundColor Cyan
|
||||
$gateExit = 0
|
||||
try {
|
||||
& $Action
|
||||
if ($LASTEXITCODE -ne 0) { $gateExit = $LASTEXITCODE }
|
||||
} catch {
|
||||
Write-Error $_
|
||||
$gateExit = 127
|
||||
}
|
||||
if ($gateExit -ne 0) {
|
||||
$failures.Add("$Name (exit $gateExit)")
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host 'Shanyuan Danyang V1.1 - Phase 01 baseline verification' -ForegroundColor Green
|
||||
Write-Host "Project root: $projectRoot"
|
||||
|
||||
Invoke-Gate 'Git repository' {
|
||||
git -C $projectRoot status --short --branch
|
||||
}
|
||||
|
||||
if (-not $SkipBackend) {
|
||||
Invoke-Gate 'Backend unit tests' {
|
||||
$java = Get-Command java -ErrorAction SilentlyContinue
|
||||
$maven = Get-Command mvn -ErrorAction SilentlyContinue
|
||||
if ($java -and $maven) {
|
||||
$mavenRepo = Join-Path $projectRoot 'backend/local-maven'
|
||||
& $maven.Source -s (Join-Path $projectRoot 'backend/maven-settings.xml') `
|
||||
"-Dmaven.repo.local=$mavenRepo" `
|
||||
-f (Join-Path $projectRoot 'backend/pom.xml') test
|
||||
} elseif (Get-Command docker -ErrorAction SilentlyContinue) {
|
||||
$mount = "${projectRoot}:/workspace"
|
||||
$containerCommand = 'rm -rf /tmp/maven && mkdir -p /tmp/maven && cd /tmp/maven && jar xf /workspace/backend/maven.zip && chmod +x /tmp/maven/apache-maven-3.9.6/bin/mvn && /tmp/maven/apache-maven-3.9.6/bin/mvn -s /workspace/backend/maven-settings.xml -Dmaven.repo.local=/workspace/backend/local-maven -f /workspace/backend/pom.xml test'
|
||||
docker run --rm -v $mount -w /workspace/backend eclipse-temurin:21-jdk-alpine `
|
||||
sh -c $containerCommand
|
||||
} else {
|
||||
throw 'JDK 21 and Maven were not found, and Docker is unavailable.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-Gate 'Admin type-check' {
|
||||
Push-Location (Join-Path $projectRoot 'admin-web')
|
||||
try { npm run type-check } finally { Pop-Location }
|
||||
}
|
||||
|
||||
Invoke-Gate 'Admin production build' {
|
||||
Push-Location (Join-Path $projectRoot 'admin-web')
|
||||
try { npm run build } finally { Pop-Location }
|
||||
}
|
||||
|
||||
Invoke-Gate 'Miniapp TypeScript' {
|
||||
$tsc = Join-Path $projectRoot 'admin-web/node_modules/.bin/tsc.cmd'
|
||||
if (-not (Test-Path -LiteralPath $tsc)) {
|
||||
throw 'TypeScript compiler is missing. Run npm ci in admin-web first.'
|
||||
} else {
|
||||
& $tsc -p (Join-Path $projectRoot 'miniapp/tsconfig.json') --noEmit
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $SkipE2E) {
|
||||
Invoke-Gate 'V1.1 E2E collection and execution' {
|
||||
Push-Location $projectRoot
|
||||
try { python -m pytest tests/e2e -q } finally { Pop-Location }
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`n[Summary]" -ForegroundColor Cyan
|
||||
if ($failures.Count -eq 0) {
|
||||
Write-Host 'All Phase 01 baseline gates passed.' -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Failed gates: $($failures.Count)" -ForegroundColor Red
|
||||
$failures | ForEach-Object { Write-Host "- $_" -ForegroundColor Red }
|
||||
exit 1
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env sh
|
||||
set -uo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
FAILURE_COUNT=0
|
||||
FAILURE_NAMES=""
|
||||
|
||||
run_gate() {
|
||||
local name="$1"
|
||||
shift
|
||||
printf '\n[%s]\n' "$name"
|
||||
if ! "$@"; then
|
||||
FAILURE_COUNT=$((FAILURE_COUNT + 1))
|
||||
FAILURE_NAMES="${FAILURE_NAMES}\n- ${name}"
|
||||
fi
|
||||
}
|
||||
|
||||
backend_tests() {
|
||||
if command -v java >/dev/null 2>&1 && command -v mvn >/dev/null 2>&1; then
|
||||
mvn -s "$PROJECT_ROOT/backend/maven-settings.xml" \
|
||||
-Dmaven.repo.local="$PROJECT_ROOT/backend/local-maven" \
|
||||
-f "$PROJECT_ROOT/backend/pom.xml" test
|
||||
elif command -v docker >/dev/null 2>&1; then
|
||||
docker run --rm -v "$PROJECT_ROOT:/workspace" -w /workspace/backend \
|
||||
eclipse-temurin:21-jdk-alpine sh -c \
|
||||
'rm -rf /tmp/maven && mkdir -p /tmp/maven && cd /tmp/maven && jar xf /workspace/backend/maven.zip && chmod +x /tmp/maven/apache-maven-3.9.6/bin/mvn && /tmp/maven/apache-maven-3.9.6/bin/mvn -s /workspace/backend/maven-settings.xml -Dmaven.repo.local=/workspace/backend/local-maven -f /workspace/backend/pom.xml test'
|
||||
else
|
||||
printf '未找到 JDK 21 + Maven,且 Docker 不可用。\n' >&2
|
||||
return 127
|
||||
fi
|
||||
}
|
||||
|
||||
admin_typecheck() {
|
||||
cd "$PROJECT_ROOT/admin-web" && npm run type-check
|
||||
}
|
||||
|
||||
admin_build() {
|
||||
cd "$PROJECT_ROOT/admin-web" && npm run build
|
||||
}
|
||||
|
||||
miniapp_typecheck() {
|
||||
local tsc="$PROJECT_ROOT/admin-web/node_modules/.bin/tsc"
|
||||
if [ ! -x "$tsc" ]; then
|
||||
printf '缺少 TypeScript 编译器;请先在 admin-web 执行 npm ci。\n' >&2
|
||||
return 127
|
||||
fi
|
||||
"$tsc" -p "$PROJECT_ROOT/miniapp/tsconfig.json" --noEmit
|
||||
}
|
||||
|
||||
e2e_tests() {
|
||||
cd "$PROJECT_ROOT" && python -m pytest tests/e2e -q
|
||||
}
|
||||
|
||||
printf '善缘丹阳 V1.1 — Phase 01 基线检查\n项目目录: %s\n' "$PROJECT_ROOT"
|
||||
run_gate 'Git repository' git -C "$PROJECT_ROOT" status --short --branch
|
||||
run_gate 'Backend unit tests' backend_tests
|
||||
run_gate 'Admin type-check' admin_typecheck
|
||||
run_gate 'Admin production build' admin_build
|
||||
run_gate 'Miniapp TypeScript' miniapp_typecheck
|
||||
run_gate 'V1.1 E2E collection and execution' e2e_tests
|
||||
|
||||
printf '\n[Summary]\n'
|
||||
if [ "$FAILURE_COUNT" -eq 0 ]; then
|
||||
printf '全部 Phase 01 基线门禁通过。\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf '失败门禁: %s\n' "$FAILURE_COUNT"
|
||||
printf '%b\n' "$FAILURE_NAMES"
|
||||
exit 1
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/bash
|
||||
# V1.1 部署验证脚本
|
||||
BASE="http://localhost:8085/api/v1"
|
||||
|
||||
echo "========================================"
|
||||
echo " 善缘丹阳 V1.1 部署验证"
|
||||
echo "========================================"
|
||||
|
||||
# Check containers
|
||||
echo ""
|
||||
echo "【容器状态】"
|
||||
docker ps --format " {{.Names}}\t{{.Status}}" | grep danyang
|
||||
|
||||
# Health check
|
||||
echo ""
|
||||
echo "【API健康】"
|
||||
HEALTH=$(curl -s "$BASE/health/live")
|
||||
echo " 后端: $(echo $HEALTH | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])' 2>/dev/null || echo 'FAIL')"
|
||||
ADMIN_CODE=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8086/)
|
||||
echo " Admin: HTTP $ADMIN_CODE"
|
||||
|
||||
# Login and test. Credentials must be supplied by the caller; never embed defaults.
|
||||
: "${OPERATOR_USERNAME:?请设置 OPERATOR_USERNAME}"
|
||||
: "${OPERATOR_PASSWORD:?请设置 OPERATOR_PASSWORD}"
|
||||
LOGIN_BODY=$(python3 -c 'import json,os; print(json.dumps({"username":os.environ["OPERATOR_USERNAME"],"password":os.environ["OPERATOR_PASSWORD"]}))')
|
||||
TOKEN=$(curl -s -X POST "$BASE/auth/operator/login" -H "Content-Type: application/json" -d "$LOGIN_BODY" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["token"])')
|
||||
H="Authorization: Bearer $TOKEN"
|
||||
|
||||
echo ""
|
||||
echo "【核心API验证】"
|
||||
for endpoint in "/home" "/content/categories" "/operator/analytics/v11-dashboard" "/operator/feature-flags"; do
|
||||
STATUS=$(curl -s -H "$H" "$BASE$endpoint" | python3 -c 'import sys,json;d=json.load(sys.stdin);print("OK" if d.get("success") else "FAIL")' 2>/dev/null)
|
||||
echo " $endpoint → $STATUS"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "【数据库记录】"
|
||||
docker exec danyang-match-postgres psql -U postgres -d danyang_match -t -c \
|
||||
"SELECT ' operators: ' || COUNT(*) FROM operators UNION ALL SELECT ' users: ' || COUNT(*) FROM users UNION ALL SELECT ' banners: ' || COUNT(*) FROM home_banners UNION ALL SELECT ' categories: ' || COUNT(*) FROM content_categories;" 2>/dev/null
|
||||
|
||||
echo ""
|
||||
echo "✅ 验证完成"
|
||||
Reference in New Issue
Block a user